From 8965989b548e7bb2d3b850f67425380204e5d4b0 Mon Sep 17 00:00:00 2001 From: Michal Berner Date: Mon, 4 May 2026 09:01:04 +0200 Subject: [PATCH] ci: Sign Windows release artifacts Split release builds from packaging so Windows executables can be signed before archives and checksums are created. The release workflow now builds raw binaries, signs Windows artifacts with Azure Artifact Signing via GitHub Actions OIDC, verifies Authenticode signatures, packages the signed binaries, and publishes checksums for the final archives. Document the required release-signing environment, Azure secrets, workflow variables, and certificate profile role. --- .github/workflows/release.yml | 116 +++++++++++++++++++++++++++++++--- RELEASING.md | 34 ++++++++-- scripts/build.sh | 32 ++-------- scripts/package.sh | 67 ++++++++++++++++++++ 4 files changed, 209 insertions(+), 40 deletions(-) create mode 100755 scripts/package.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96a442e..ccb17a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,12 +7,14 @@ on: jobs: build: - name: Build and Release + name: Build binaries runs-on: ubuntu-latest - + permissions: + contents: read + steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history for version info @@ -21,21 +23,115 @@ jobs: with: go-version: '1.21' - - name: Make build script executable - run: chmod +x scripts/build.sh + - name: Make scripts executable + run: chmod +x scripts/build.sh scripts/package.sh + + - name: Build binaries + run: SKIP_PACKAGE=1 ./scripts/build.sh ${GITHUB_REF#refs/tags/} + + - name: Upload unsigned build artifacts + uses: actions/upload-artifact@v4 + with: + name: unsigned-dist + path: dist/* + if-no-files-found: error + retention-days: 1 + + sign-windows: + name: Sign Windows binaries + runs-on: windows-2022 + needs: build + environment: release-signing + permissions: + contents: read + id-token: write + + steps: + - name: Download unsigned build artifacts + uses: actions/download-artifact@v4 + with: + name: unsigned-dist + path: dist + + - name: Azure login + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Sign Windows binaries with Azure Artifact Signing + uses: azure/artifact-signing-action@v1 + with: + endpoint: ${{ vars.AZURE_CODESIGN_ENDPOINT }} + signing-account-name: ${{ vars.AZURE_CODESIGN_ACCOUNT_NAME }} + certificate-profile-name: ${{ vars.AZURE_CODESIGN_CERTIFICATE_PROFILE }} + files-folder: ${{ github.workspace }}\dist + files-folder-filter: git-flow-*-windows-*.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Verify Windows signatures + shell: pwsh + run: | + $files = Get-ChildItem -Path "$env:GITHUB_WORKSPACE\dist" -Filter "git-flow-*-windows-*.exe" + if ($files.Count -eq 0) { + throw "No Windows executables found to verify." + } + + foreach ($file in $files) { + $signature = Get-AuthenticodeSignature -FilePath $file.FullName + if ($signature.Status -ne "Valid") { + Write-Error "Invalid signature for $($file.Name): $($signature.Status) $($signature.StatusMessage)" + exit 1 + } + Write-Host "Verified signature for $($file.Name)" + } + + - name: Upload signed build artifacts + uses: actions/upload-artifact@v4 + with: + name: signed-dist + path: dist/* + if-no-files-found: error + retention-days: 1 + + release: + name: Package and release + runs-on: ubuntu-latest + needs: sign-windows + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download signed build artifacts + uses: actions/download-artifact@v4 + with: + name: signed-dist + path: dist + + - name: Make packaging script executable + run: chmod +x scripts/package.sh + + - name: Restore Unix executable permissions + run: chmod +x dist/git-flow-*-darwin-* dist/git-flow-*-linux-* + + - name: Package signed artifacts + run: ./scripts/package.sh ${GITHUB_REF#refs/tags/} - name: Check if preview release id: check_preview run: | - if [[ ${{ github.ref }} =~ ^refs/tags/v.*-(alpha|beta|rc)\. ]]; then + if [[ "${GITHUB_REF_NAME}" =~ ^v.*-(alpha|beta|rc)\. ]]; then echo "is_preview=true" >> $GITHUB_OUTPUT else echo "is_preview=false" >> $GITHUB_OUTPUT fi - - name: Build binaries - run: ./scripts/build.sh ${GITHUB_REF#refs/tags/} - - name: Extract release notes from CHANGELOG id: changelog run: | @@ -79,4 +175,4 @@ jobs: ## Checksums SHA-256 checksums for the release artifacts are available in the checksums.txt file. env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/RELEASING.md b/RELEASING.md index ae89e0f..c192535 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -39,6 +39,28 @@ We follow [Keep a Changelog](https://keepachangelog.com/). Only **user-facing ch ## Release Process +### Release Signing Setup + +Windows release executables are signed with Azure Artifact Signing before archives and checksums are created. The release workflow uses GitHub Actions OIDC. + +Configure a GitHub environment named `release-signing` with these secrets: + +- `AZURE_CLIENT_ID` +- `AZURE_TENANT_ID` +- `AZURE_SUBSCRIPTION_ID` + +Configure the same environment with these variables: + +- `AZURE_CODESIGN_ENDPOINT` +- `AZURE_CODESIGN_ACCOUNT_NAME` +- `AZURE_CODESIGN_CERTIFICATE_PROFILE` + +The Azure identity must have the `Artifact Signing Certificate Profile Signer` role on the certificate profile. Configure the Entra federated credential for this GitHub environment subject: + +```text +repo:gittower/git-flow-next:environment:release-signing +``` + ### 1. Determine Version Bump Review commits since last release: @@ -93,10 +115,13 @@ git push origin vX.Y.Z The `.github/workflows/release.yml` workflow automatically: -1. Builds binaries for all platforms (darwin, linux, windows) -2. Creates GitHub release with artifacts -3. Generates checksums -4. Marks as prerelease if tag contains `-alpha`, `-beta`, or `-rc` +1. Builds raw binaries for all platforms (darwin, linux, windows) +2. Signs Windows `.exe` files with Azure Artifact Signing +3. Verifies Windows Authenticode signatures +4. Creates archives containing the signed Windows executables +5. Generates checksums after signing and packaging +6. Creates GitHub release with artifacts +7. Marks as prerelease if tag contains `-alpha`, `-beta`, or `-rc` ### 7. Update Homebrew Tap @@ -143,6 +168,7 @@ These are automatically marked as prereleases on GitHub. - [ ] Committed: `chore: Bump version to X.Y.Z` - [ ] Pushed to main - [ ] Created and pushed tag +- [ ] Verified Windows signing job succeeded - [ ] Verified GitHub release was created - [ ] Updated Homebrew tap (`./update_formula.rb`) - [ ] Updated website documentation (if commands changed) diff --git a/scripts/build.sh b/scripts/build.sh index 7775484..cc783be 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -euo pipefail + # Get version from command line or use "dev" as default VERSION=${1:-dev} BINARY_NAME="git-flow" @@ -64,31 +66,9 @@ if [[ ${#MISSING_BINARIES[@]} -gt 0 ]]; then exit 1 fi -# Create archives for each binary -echo "Creating archives..." - -# macOS -echo "Creating darwin archives..." -tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-darwin-amd64" -tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-darwin-arm64" - -# Linux -echo "Creating linux archives..." -tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-amd64" -tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-arm64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-arm64" -tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-386.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-386" - -# Windows (using zip instead of tar.gz) -echo "Creating windows archives..." -if command -v zip >/dev/null 2>&1; then - (cd "$BUILD_DIR" && zip "${PACKAGE_NAME}-${VERSION}-windows-amd64.zip" "${BINARY_NAME}-${VERSION}-windows-amd64.exe") - (cd "$BUILD_DIR" && zip "${PACKAGE_NAME}-${VERSION}-windows-386.zip" "${BINARY_NAME}-${VERSION}-windows-386.exe") +if [[ "${SKIP_PACKAGE:-}" == "1" ]]; then + echo "Build complete! Binaries are in the $BUILD_DIR directory" else - echo "Warning: zip command not found, skipping Windows archives" + "$(dirname "$0")/package.sh" "$VERSION" + echo "Build complete! Artifacts are in the $BUILD_DIR directory" fi - -# Generate checksums -echo "Generating checksums..." -(cd "$BUILD_DIR" && shasum -a 256 * > "${PACKAGE_NAME}-${VERSION}-checksums.txt") - -echo "Build complete! Artifacts are in the $BUILD_DIR directory" \ No newline at end of file diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 0000000..5a2e626 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -euo pipefail + +# Get version from command line or use "dev" as default +VERSION=${1:-dev} +BINARY_NAME="git-flow" +PACKAGE_NAME="git-flow-next" + +# Build directory +BUILD_DIR="dist" + +echo "Packaging $PACKAGE_NAME version $VERSION..." + +# Verify all binaries are present before packaging +echo "Verifying binaries..." +MISSING_BINARIES=() + +for binary in "${BINARY_NAME}-${VERSION}-darwin-amd64" "${BINARY_NAME}-${VERSION}-darwin-arm64" \ + "${BINARY_NAME}-${VERSION}-linux-amd64" "${BINARY_NAME}-${VERSION}-linux-arm64" "${BINARY_NAME}-${VERSION}-linux-386" \ + "${BINARY_NAME}-${VERSION}-windows-amd64.exe" "${BINARY_NAME}-${VERSION}-windows-386.exe"; do + if [[ ! -f "$BUILD_DIR/$binary" ]]; then + MISSING_BINARIES+=("$binary") + fi +done + +if [[ ${#MISSING_BINARIES[@]} -gt 0 ]]; then + echo "Error: The following binaries were not found:" + for binary in "${MISSING_BINARIES[@]}"; do + echo " - $binary" + done + exit 1 +fi + +# Remove generated artifacts for this version so checksums describe the final outputs. +rm -f "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-"*.tar.gz \ + "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-"*.zip \ + "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-checksums.txt" + +# Create archives for each binary +echo "Creating archives..." + +# macOS +echo "Creating darwin archives..." +tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-darwin-amd64" +tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-darwin-arm64" + +# Linux +echo "Creating linux archives..." +tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-amd64" +tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-arm64.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-arm64" +tar czf "$BUILD_DIR/${PACKAGE_NAME}-${VERSION}-linux-386.tar.gz" -C "$BUILD_DIR" "${BINARY_NAME}-${VERSION}-linux-386" + +# Windows (using zip instead of tar.gz) +echo "Creating windows archives..." +if command -v zip >/dev/null 2>&1; then + (cd "$BUILD_DIR" && zip "${PACKAGE_NAME}-${VERSION}-windows-amd64.zip" "${BINARY_NAME}-${VERSION}-windows-amd64.exe") + (cd "$BUILD_DIR" && zip "${PACKAGE_NAME}-${VERSION}-windows-386.zip" "${BINARY_NAME}-${VERSION}-windows-386.exe") +else + echo "Warning: zip command not found, skipping Windows archives" +fi + +# Generate checksums +echo "Generating checksums..." +(cd "$BUILD_DIR" && shasum -a 256 * > "${PACKAGE_NAME}-${VERSION}-checksums.txt") + +echo "Packaging complete! Artifacts are in the $BUILD_DIR directory"