From 7694a9f52b905058a826d496894296b63b78112d Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 14 Aug 2026 12:49:17 -0700 Subject: [PATCH] fix: Package-and-Upload workflow attaches binaries to UI-created releases Two bugs found while investigating why v2.0.21 shipped without executables attached: 1. The "Upload binaries to release" step was gated on `github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')`. Releases created via the GitHub UI create a tag but do not fire the push:tags: event that this workflow depends on, so v2.0.21 (and v2.0.19) ended up as releases with no assets. The only successful run for v2.0.21 was a workflow_dispatch afterward that built the binaries as artifacts but skipped the upload-to-release step. 2. Both macOS build jobs used `name: tabcmd-macos` on `actions/upload-artifact`, so the two same-named artifacts collided in the artifact store and downloads clobbered each other. Fixes: - New `release_tag` workflow_dispatch input. Set it when dispatching from a branch to attach binaries to a UI-created release. Falls back to `github.ref_name` when the workflow runs on a tag ref (push or dispatch). - Fix mac artifact collision: use `matrix.UPLOAD_FILE_NAME` (unique per platform) as the artifact name instead of `tabcmd-${{ matrix.TARGET }}`. - Split the upload into a separate `upload_to_release` job that depends on `buildexe`, gated on `environment: release`. The `release` environment needs to be created in the repo's Settings -> Environments with required-reviewer protection (mirroring the existing `pypi` environment). Anyone with dispatch access can trigger a build, but only an approved reviewer can attach binaries to a public release. - Upload gate: `if: startsWith(github.ref, 'refs/tags/') || inputs.release_tag != ''` so both the push:tags path and the workflow_dispatch path work. Live-verified by using `gh release upload` today to fix v2.0.21 retroactively with the artifacts from the last workflow_dispatch run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/package.yml | 82 ++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 9413ed6a..058e2692 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -13,11 +13,21 @@ on: tags: - '*' workflow_dispatch: + inputs: + release_tag: + description: > + Tag of an existing release to attach build artifacts to. Leave blank + when dispatching on a tag ref (github.ref_name is used). Set this + when dispatching from a branch to attach binaries to a release + created via the GitHub UI (which does not fire the push:tags: event + that would trigger this workflow automatically). + required: false + default: '' jobs: buildexe: - name: Build executables and upload them to the existing release + name: Build executables runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -97,18 +107,76 @@ jobs: tar -cvf ${{ matrix.UPLOAD_FILE_NAME }} ${{ matrix.OUT_FILE_NAME }} + # UPLOAD_FILE_NAME distinguishes the two macOS artifacts (x86 vs arm64); + # the shared artifact name `tabcmd-macos` would otherwise collide and each + # upload would clobber the other in the artifact store. - name: Upload build artifact for ${{ matrix.TARGET }} uses: actions/upload-artifact@v7 with: - name: tabcmd-${{ matrix.TARGET }} + name: ${{ matrix.UPLOAD_FILE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.UPLOAD_FILE_NAME }} - - name: Upload binaries to release for ${{ matrix.TARGET }} - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + # Attach the built binaries to the target GitHub release. Split into its own + # job so we can gate it behind the `release` environment: a required-reviewer + # protection on that environment means anyone with dispatch access can + # trigger a build, but only an approved reviewer can actually attach binaries + # to a public release. On push:tags this still runs but the approval step + # will pause the workflow until a reviewer clicks Approve. + # + # `needs: buildexe` waits for ALL matrix legs to succeed. If any leg fails + # this job is skipped (default behavior with no `if: always()`), so a partial + # release upload where e.g. macOS is missing is never possible. + upload_to_release: + name: Attach build artifacts to release + needs: buildexe + if: startsWith(github.ref, 'refs/tags/') || inputs.release_tag != '' + runs-on: ubuntu-latest + environment: release + steps: + # upload-artifact@v7 pairs with download-artifact@v8: the two action majors + # don't move in lockstep. v8 of download-artifact adds hash-mismatch-errors + # and direct-download support; there is no v8 of upload-artifact yet. + - name: Download all build artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts/ + + # Upload runs on both push:tags and workflow_dispatch. For push:tags, + # github.ref_name is the tag. For workflow_dispatch, use the release_tag + # input if set (release created via GitHub UI), else fall back to + # github.ref_name (workflow dispatched on a tag ref). + - name: Upload tabcmd.exe (Windows) to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: tabcmd.exe + file: artifacts/tabcmd.exe/tabcmd.exe + tag: ${{ inputs.release_tag || github.ref_name }} + overwrite: true + + - name: Upload tabcmd (Ubuntu) to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: tabcmd + file: artifacts/tabcmd/tabcmd + tag: ${{ inputs.release_tag || github.ref_name }} + overwrite: true + + - name: Upload tabcmd-x86.app.tar (macOS x86) to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: tabcmd-x86.app.tar + file: artifacts/tabcmd-x86.app.tar/tabcmd-x86.app.tar + tag: ${{ inputs.release_tag || github.ref_name }} + overwrite: true + + - name: Upload tabcmd_arm64.app.tar (macOS ARM64) to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - asset_name: ${{ matrix.UPLOAD_FILE_NAME }} - file: ./dist/${{ matrix.TARGET }}/${{ matrix.UPLOAD_FILE_NAME }} - tag: ${{ github.ref_name }} + asset_name: tabcmd_arm64.app.tar + file: artifacts/tabcmd_arm64.app.tar/tabcmd_arm64.app.tar + tag: ${{ inputs.release_tag || github.ref_name }} overwrite: true