From 7694a9f52b905058a826d496894296b63b78112d Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 14 Aug 2026 12:49:17 -0700 Subject: [PATCH 1/2] 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 From ca9d74b0418febefc633a1bc5b5280bd23dc9032 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 12:56:13 -0700 Subject: [PATCH 2/2] Harden Package-and-Upload workflow per review - Switch to release: types: [published] as the trigger (matches publish-pypi.yml pattern). Removes the manual-dispatch-with-input footgun; release payload provides the tag directly. - Add explicit permissions: contents: write on the upload job so it survives future org-default hardening. - Add concurrency group keyed on the release tag; prevents overlapping runs from stomping each other's approved uploads. - Pin download-artifact back to v7 to match upload-artifact@v7 until v7/v8 interop is verified. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/package.yml | 50 +++++++++++++---------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 058e2692..87c453a4 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -9,20 +9,12 @@ name: Package-and-Upload # https://anshumanfauzdar.medium.com/using-github-actions-to-bundle-python-application-into-a-single-package-and-automatic-release-834bd42e0670 on: - push: - 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: '' + release: + types: [published] + +concurrency: + group: package-${{ github.event.release.tag_name }} + cancel-in-progress: false jobs: @@ -118,10 +110,9 @@ jobs: # 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. + # protection on that environment means publishing a release triggers a build, + # but only an approved reviewer can actually attach binaries to that release. + # The workflow pauses at this job 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 @@ -129,29 +120,26 @@ jobs: 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 + permissions: + contents: write 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. + # v7 to match upload-artifact@v7; bump both together - name: Download all build artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 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). + # The `release: published` trigger provides the tag directly via + # github.event.release.tag_name - no fallback needed. - 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 }} + tag: ${{ github.event.release.tag_name }} overwrite: true - name: Upload tabcmd (Ubuntu) to release @@ -160,7 +148,7 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: tabcmd file: artifacts/tabcmd/tabcmd - tag: ${{ inputs.release_tag || github.ref_name }} + tag: ${{ github.event.release.tag_name }} overwrite: true - name: Upload tabcmd-x86.app.tar (macOS x86) to release @@ -169,7 +157,7 @@ jobs: 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 }} + tag: ${{ github.event.release.tag_name }} overwrite: true - name: Upload tabcmd_arm64.app.tar (macOS ARM64) to release @@ -178,5 +166,5 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: tabcmd_arm64.app.tar file: artifacts/tabcmd_arm64.app.tar/tabcmd_arm64.app.tar - tag: ${{ inputs.release_tag || github.ref_name }} + tag: ${{ github.event.release.tag_name }} overwrite: true