Build and Release #97
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| # Manual release flow (modeled on gruntwork-io/terragrunt's release.yml): | |
| # | |
| # 1. A maintainer creates a DRAFT GitHub release for the version, targeting a | |
| # specific commit (not a branch). The draft holds the release notes. | |
| # 2. They run this workflow via "Run workflow", passing that version. | |
| # 3. `validate` confirms the draft exists, the tag is strict semver, and the | |
| # draft targets a full commit SHA (so no new commits can slip in between | |
| # draft creation and publishing). | |
| # 4. The per-platform `build-*` jobs build and package from that exact commit. | |
| # 5. `upload-assets` attaches the binaries + checksums to the draft release. | |
| # 6. The maintainer publishes the draft from the Releases page when ready. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to build (must match an existing draft release, e.g. v1.0.2)" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Validate the release is an existing release with a valid semver tag, | |
| # targeting an explicit commit SHA. | |
| validate: | |
| name: Validate Release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.resolve.outputs.version }} | |
| ref: ${{ steps.resolve.outputs.ref }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Resolve version and ref | |
| id: resolve | |
| env: | |
| INPUT_VERSION: ${{ inputs.version }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: .github/scripts/release/resolve-version-ref.sh | |
| - name: Validate semver | |
| env: | |
| VERSION: ${{ steps.resolve.outputs.version }} | |
| run: .github/scripts/release/validate-semver.sh "$VERSION" | |
| - name: Enforce commit SHA target | |
| env: | |
| REF: ${{ steps.resolve.outputs.ref }} | |
| run: .github/scripts/release/enforce-commit-sha.sh "$REF" | |
| build-macos: | |
| name: Build macOS | |
| needs: validate | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.validate.outputs.ref }} | |
| - uses: jdx/mise-action@dba19683ed58901619b14f395a24841710cb4925 # v4.1.0 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build Electron app | |
| run: just build | |
| # Build both arches in a single job, one arch at a time. This must NOT be | |
| # a matrix and must NOT be a single `electron-builder --mac` that emits | |
| # both arches at once, because: | |
| # 1. `resources/bin/boilerplate` is a single, arch-specific binary | |
| # (fetch-boilerplate deletes the alternate). Each arch must be | |
| # packaged with its matching boilerplate, so we re-fetch between | |
| # passes — a both-at-once build would bundle the wrong-arch CLI. | |
| # 2. A matrix had each job build BOTH arches anyway (the package.json | |
| # `mac.target` arch arrays override the `--arm64`/`--x64` flag), so | |
| # every artifact was produced twice under two artifact bundles. The | |
| # create-release `download-artifact ... merge-multiple: true` then | |
| # extracted the colliding filenames concurrently into one dir, which | |
| # tore the bytes of whichever copy lost the race and shipped a DMG | |
| # that failed `hdiutil verify` ("disk not readable"). One job, one | |
| # copy of each artifact, one upload bundle = no collision. | |
| # The `arch` arrays were removed from package.json `mac.target` so the | |
| # `--arm64`/`--x64` flag below actually restricts each pass to one arch. | |
| - name: Package for macOS | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ secrets.MACOS_AC_LOGIN }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.MACOS_AC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.MACOS_AC_PROVIDER }} | |
| run: | | |
| set -euo pipefail | |
| # Only enable code signing when a certificate is actually configured. | |
| # An empty CSC_LINK is treated by electron-builder as a file path that | |
| # resolves to the project root, failing with "<projectDir> not a file". | |
| if [ -n "$MACOS_CERTIFICATE" ]; then | |
| export CSC_LINK="$MACOS_CERTIFICATE" | |
| export CSC_KEY_PASSWORD="$MACOS_CERTIFICATE_PASSWORD" | |
| else | |
| echo "No MACOS_CERTIFICATE secret set; building an unsigned macOS app." | |
| export CSC_IDENTITY_AUTO_DISCOVERY=false | |
| fi | |
| # arm64 pass (boilerplate arch arm64). `--publish never`: the | |
| # upload-assets job is the sole uploader; electron-builder must not | |
| # also push to the release (its implicit tag-publish caused duplicate, | |
| # racing uploads of identically named assets). | |
| just fetch-boilerplate darwin arm64 | |
| mise x node -- npx electron-builder --mac --arm64 --publish never | |
| # Preserve the arm64 auto-update manifest: the x64 pass below rewrites | |
| # latest-mac.yml from scratch with only its own artifacts (electron- | |
| # builder only merges manifests when publishing, and we use | |
| # `--publish never`), so without this the arm64 entries are lost. | |
| cp out/latest-mac.yml out/latest-mac-arm64.yml | |
| # x64 pass (boilerplate arch amd64). Cross-built on the arm64 runner. | |
| just fetch-boilerplate darwin amd64 | |
| mise x node -- npx electron-builder --mac --x64 --publish never | |
| # Merge the arm64 entries back into the now-x64-only latest-mac.yml so | |
| # the published manifest lists both arches. electron-updater picks the | |
| # arch by scanning files[] for an "arm64" url; an arch-incomplete | |
| # manifest serves arm64 Macs the x64 build (which then runs under | |
| # Rosetta). arm64 first so the legacy top-level path/sha512 match a | |
| # single both-arch build. | |
| mise x node -- node .github/scripts/release/merge-mac-latest.mjs \ | |
| out/latest-mac.yml out/latest-mac-arm64.yml out/latest-mac.yml | |
| rm -f out/latest-mac-arm64.yml | |
| # Fail the build loudly if any artifact is corrupt, instead of shipping a | |
| # bad file with a matching checksum (the original failure mode). | |
| - name: Verify macOS artifacts | |
| run: | | |
| set -euo pipefail | |
| for f in out/*.dmg; do | |
| echo "Verifying $f" | |
| hdiutil verify "$f" | |
| done | |
| for z in out/*.zip; do | |
| echo "Testing $z" | |
| unzip -t "$z" >/dev/null | |
| done | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: macos | |
| path: | | |
| out/*.dmg | |
| out/*.zip | |
| out/latest-mac*.yml | |
| retention-days: 7 | |
| build-linux: | |
| name: Build Linux | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.validate.outputs.ref }} | |
| - uses: jdx/mise-action@dba19683ed58901619b14f395a24841710cb4925 # v4.1.0 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build Electron app | |
| run: just build | |
| - name: Fetch boilerplate | |
| run: just fetch-boilerplate linux amd64 | |
| # `--publish never`: the upload-assets job is the sole uploader. | |
| - name: Package for Linux | |
| run: mise x node -- npx electron-builder --linux --publish never | |
| - name: Upload Linux artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: linux-x64 | |
| path: | | |
| out/*.AppImage | |
| out/*.deb | |
| out/latest-linux*.yml | |
| retention-days: 7 | |
| build-windows: | |
| name: Build Windows | |
| needs: validate | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.validate.outputs.ref }} | |
| - uses: jdx/mise-action@dba19683ed58901619b14f395a24841710cb4925 # v4.1.0 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build Electron app | |
| run: just build | |
| - name: Fetch boilerplate | |
| shell: bash | |
| run: just fetch-boilerplate windows amd64 | |
| # `--publish never`: the upload-assets job is the sole uploader. | |
| - name: Package for Windows | |
| run: mise x node -- npx electron-builder --win --publish never | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: windows-x64 | |
| path: | | |
| out/*.exe | |
| out/latest*.yml | |
| retention-days: 7 | |
| # Attach the built binaries + checksums to the existing draft release. | |
| upload-assets: | |
| name: Upload Release Assets | |
| needs: [validate, build-macos, build-linux, build-windows] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.validate.outputs.ref }} | |
| - name: Check if release exists | |
| id: check_release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: .github/scripts/release/check-release-exists.sh | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts/ | |
| merge-multiple: true | |
| - name: Generate checksums | |
| run: .github/scripts/release/generate-checksums.sh artifacts | |
| - name: Upload assets to release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| CLOBBER: "true" | |
| run: .github/scripts/release/upload-assets.sh artifacts | |
| - name: Verify all assets uploaded | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| CLOBBER: "true" | |
| run: .github/scripts/release/verify-assets-uploaded.sh artifacts | |
| - name: Upload summary | |
| if: always() | |
| env: | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| RELEASE_ID: ${{ steps.check_release.outputs.release_id }} | |
| IS_DRAFT: ${{ steps.check_release.outputs.is_draft }} | |
| run: .github/scripts/release/generate-upload-summary.sh artifacts |