From e33b651ab43f2c81020fe0727eb5861dc0765957 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 14 Aug 2026 12:49:17 -0700 Subject: [PATCH 1/6] 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 04e564ca578895fbccb44ab3644846a203d4b58c Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 10 Sep 2026 12:56:13 -0700 Subject: [PATCH 2/6] 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 From 499068491823d11441e910e07890a0e4d97be5f3 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 18 Sep 2026 18:09:46 -0700 Subject: [PATCH 3/6] ci(package): restore workflow_dispatch + resolve release tag across event paths Copilot flagged that removing workflow_dispatch broke the release-on-merge.yml caller and the documented manual re-attach path, and that github.event.release.tag_name is empty for anything other than release:published events. Restores workflow_dispatch alongside release:published, adds a "Resolve release tag" step that falls back to github.ref_name, and switches all four uploaders to the resolved output. --- .github/workflows/package.yml | 39 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 87c453a4..7251e3e5 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -11,9 +11,13 @@ name: Package-and-Upload on: release: types: [published] + # Manual repair path + release-on-merge.yml's `gh workflow run package.yml + # --ref ` invocation. The tag is resolved from github.ref_name when the + # release event isn't the trigger. + workflow_dispatch: concurrency: - group: package-${{ github.event.release.tag_name }} + group: package-${{ github.event.release.tag_name || github.ref_name }} cancel-in-progress: false jobs: @@ -131,15 +135,36 @@ jobs: with: path: artifacts/ - # The `release: published` trigger provides the tag directly via - # github.event.release.tag_name - no fallback needed. + # Resolve the release tag from whichever event triggered us: `release: + # published` populates github.event.release.tag_name; workflow_dispatch + # (invoked with `--ref `) leaves that empty and github.ref_name + # carries the tag instead. Fail fast if neither is set so we never + # attempt an upload against an empty tag. + - name: Resolve release tag + id: tag + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + run: | + TAG="$RELEASE_TAG" + if [ -z "$TAG" ]; then + TAG="$REF_NAME" + fi + if [ -z "$TAG" ]; then + echo "::error::No release tag resolvable (event=$EVENT_NAME, release.tag_name empty, ref_name empty)" + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Resolved release tag: $TAG" + - 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: ${{ github.event.release.tag_name }} + tag: ${{ steps.tag.outputs.tag }} overwrite: true - name: Upload tabcmd (Ubuntu) to release @@ -148,7 +173,7 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: tabcmd file: artifacts/tabcmd/tabcmd - tag: ${{ github.event.release.tag_name }} + tag: ${{ steps.tag.outputs.tag }} overwrite: true - name: Upload tabcmd-x86.app.tar (macOS x86) to release @@ -157,7 +182,7 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: tabcmd-x86.app.tar file: artifacts/tabcmd-x86.app.tar/tabcmd-x86.app.tar - tag: ${{ github.event.release.tag_name }} + tag: ${{ steps.tag.outputs.tag }} overwrite: true - name: Upload tabcmd_arm64.app.tar (macOS ARM64) to release @@ -166,5 +191,5 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: tabcmd_arm64.app.tar file: artifacts/tabcmd_arm64.app.tar/tabcmd_arm64.app.tar - tag: ${{ github.event.release.tag_name }} + tag: ${{ steps.tag.outputs.tag }} overwrite: true From 04c0fa2c8db8689b24f7ada531dcd608b620a654 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 18 Sep 2026 18:23:46 -0700 Subject: [PATCH 4/6] docs(contributing): document release-publish reviewer gate + manual re-attach Adds a Publishing-a-release subsection covering the release:published path fired by publishing the draft release in the UI, the new `environment: release` reviewer-approval gate that pauses upload_to_release until a maintainer clicks "Review deployments", and the manual re-attach command `gh workflow run package.yml --ref ` for existing releases. Requested during Copilot review of #459. --- contributing.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/contributing.md b/contributing.md index 97ee80d9..7a71c989 100644 --- a/contributing.md +++ b/contributing.md @@ -11,6 +11,7 @@ * [Releases](#releases) * [Versioning](#versioning) * [Packaging](#packaging) + * [Publishing a release](#publishing-a-release) ## Install Tabcmd @@ -176,6 +177,25 @@ To run the newly created executable, from a console window in the same directory To investigate what's packaged in the executable, use https://pyinstxtractor-web.netlify.app/ +### Publishing a release + +Merging into `main` triggers `release-on-merge.yml`, which creates a **draft** release, pushes the version tag, and dispatches `package.yml` to build the platform binaries. + +When you then **publish** that draft release in the GitHub UI: + +1. `package.yml` fires again on the `release: published` event and rebuilds the binaries against the exact tagged commit. +2. Before binaries are attached, the workflow **pauses at the `upload_to_release` job** waiting for a reviewer to approve the `release` environment. Go to **Actions → the latest `Package-and-Upload` run → Review deployments → Approve**. The four binaries (Windows `.exe`, Ubuntu `tabcmd`, macOS x86 and arm64 `.app.tar`) get attached after approval. +3. Separately, `publish-pypi.yml` uploads the wheel to PyPI, also gated on the `release` environment. + +If binaries need to be re-attached to an existing release (e.g. one build leg originally failed), dispatch the workflow at the target tag: + +```shell +gh workflow run package.yml --ref v2.1.0 +``` + +Same reviewer gate applies. + + ## Release process 1. Create a new Github project release manually: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases From ed24757e2d0e1b3f45b1ead57df45528aa2f1af3 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 23 Oct 2026 13:37:03 -0700 Subject: [PATCH 5/6] fix(workflow): add release_tag dispatch input and fail fast on branch dispatch without one Address Copilot review on #459: define the release_tag workflow_dispatch input, include it in the concurrency group, and resolve the upload tag from the release event, then release_tag, then ref_name only for tag refs. A branch dispatch with no release_tag now errors instead of using the branch name. Correct contributing.md to say publish-pypi.yml is gated on the pypi environment, and document the branch-dispatch form. Co-Authored-By: Claude Code --- .github/workflows/package.yml | 32 +++++++++++++++++++++++--------- contributing.md | 4 +++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 7251e3e5..95780abe 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -12,12 +12,17 @@ on: release: types: [published] # Manual repair path + release-on-merge.yml's `gh workflow run package.yml - # --ref ` invocation. The tag is resolved from github.ref_name when the - # release event isn't the trigger. + # --ref ` invocation. The release tag is resolved from the release event, + # then inputs.release_tag, then github.ref_name when the ref is a tag. workflow_dispatch: + inputs: + release_tag: + description: 'Release tag to attach binaries to. Required when dispatching from a branch rather than a tag ref.' + required: false + type: string concurrency: - group: package-${{ github.event.release.tag_name || github.ref_name }} + group: package-${{ github.event.release.tag_name || inputs.release_tag || github.ref_name }} cancel-in-progress: false jobs: @@ -135,24 +140,33 @@ jobs: with: path: artifacts/ - # Resolve the release tag from whichever event triggered us: `release: - # published` populates github.event.release.tag_name; workflow_dispatch - # (invoked with `--ref `) leaves that empty and github.ref_name - # carries the tag instead. Fail fast if neither is set so we never - # attempt an upload against an empty tag. + # Resolve the release tag, in order of preference: the release event's + # tag_name, the workflow_dispatch release_tag input, then github.ref_name + # when the run was dispatched from a tag ref. A dispatch from a branch + # with no release_tag fails fast rather than treating the branch name as + # a tag, and an empty result never reaches the upload steps. - name: Resolve release tag id: tag env: RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ inputs.release_tag }} REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} EVENT_NAME: ${{ github.event_name }} run: | TAG="$RELEASE_TAG" if [ -z "$TAG" ]; then + TAG="$INPUT_TAG" + fi + if [ -z "$TAG" ] && [ "$REF_TYPE" = "tag" ]; then TAG="$REF_NAME" fi + if [ -z "$TAG" ] && [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "::error::workflow_dispatch from $REF_TYPE '$REF_NAME' has no release_tag input; pass -f release_tag= or dispatch with --ref " + exit 1 + fi if [ -z "$TAG" ]; then - echo "::error::No release tag resolvable (event=$EVENT_NAME, release.tag_name empty, ref_name empty)" + echo "::error::No release tag resolvable (event=$EVENT_NAME, release.tag_name empty, release_tag input empty, ref_type=$REF_TYPE)" exit 1 fi echo "tag=$TAG" >> "$GITHUB_OUTPUT" diff --git a/contributing.md b/contributing.md index 7a71c989..40202ce1 100644 --- a/contributing.md +++ b/contributing.md @@ -185,7 +185,7 @@ When you then **publish** that draft release in the GitHub UI: 1. `package.yml` fires again on the `release: published` event and rebuilds the binaries against the exact tagged commit. 2. Before binaries are attached, the workflow **pauses at the `upload_to_release` job** waiting for a reviewer to approve the `release` environment. Go to **Actions → the latest `Package-and-Upload` run → Review deployments → Approve**. The four binaries (Windows `.exe`, Ubuntu `tabcmd`, macOS x86 and arm64 `.app.tar`) get attached after approval. -3. Separately, `publish-pypi.yml` uploads the wheel to PyPI, also gated on the `release` environment. +3. Separately, `publish-pypi.yml` uploads the wheel to PyPI, gated on the `pypi` environment. If binaries need to be re-attached to an existing release (e.g. one build leg originally failed), dispatch the workflow at the target tag: @@ -193,6 +193,8 @@ If binaries need to be re-attached to an existing release (e.g. one build leg or gh workflow run package.yml --ref v2.1.0 ``` +To dispatch from a branch (e.g. to test workflow changes), pass the release to attach to: `gh workflow run package.yml --ref -f release_tag=v2.1.0`. A branch dispatch without `release_tag` fails at the tag-resolution step. + Same reviewer gate applies. From 5f19e094760f204570e3873ee17e6cebefb76e88 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 26 Oct 2026 14:50:42 -0700 Subject: [PATCH 6/6] Only build release binaries once a release is published Drop the package.yml dispatch from release-on-merge.yml. The merge-time run targeted a draft release, which upload-release-action cannot find by tag, and each release needed two `release` approvals. Merging to main now only pushes the tag and creates the draft; publishing it starts package.yml and publish-pypi.yml. Also rewrite the legacy release-process section of contributing.md and correct the trigger and download-artifact comments in package.yml. Co-Authored-By: Claude Code --- .github/workflows/package.yml | 13 +++++++++---- .github/workflows/release-on-merge.yml | 13 +++---------- contributing.md | 26 +++++++++++--------------- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 95780abe..d71bbb03 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -11,9 +11,13 @@ name: Package-and-Upload on: release: types: [published] - # Manual repair path + release-on-merge.yml's `gh workflow run package.yml - # --ref ` invocation. The release tag is resolved from the release event, - # then inputs.release_tag, then github.ref_name when the ref is a tag. + # Manual repair path for a published release. The release tag is resolved from + # the release event, then inputs.release_tag, then github.ref_name when + # dispatched at a tag ref. Don't dispatch for a draft release: the upload step + # finds the release by tag, drafts can't be found that way, and the action then + # creates a second release for the same tag. + # There is deliberately no `push: tags` trigger: release-on-merge.yml pushes a + # tag on every merge to main, and a build should only run once a release is published. workflow_dispatch: inputs: release_tag: @@ -134,7 +138,8 @@ jobs: permissions: contents: write steps: - # v7 to match upload-artifact@v7; bump both together + # download-artifact versions independently of upload-artifact; v7 reads the + # zipped artifacts that upload-artifact@v7 produces by default - name: Download all build artifacts uses: actions/download-artifact@v7 with: diff --git a/.github/workflows/release-on-merge.yml b/.github/workflows/release-on-merge.yml index cb324af0..7175747a 100644 --- a/.github/workflows/release-on-merge.yml +++ b/.github/workflows/release-on-merge.yml @@ -1,10 +1,9 @@ name: Draft release on merge to main # Triggers when a PR is merged into main (push to main covers all merge strategies). -# Creates a draft GitHub release with auto-generated notes, pushes the version tag, -# and explicitly dispatches package.yml to build and attach binaries. -# (Tag pushes from GITHUB_TOKEN do not trigger other workflows, so we dispatch explicitly.) -# When you publish the draft release, publish-pypi.yml uploads to PyPI automatically. +# Pushes the next patch version tag and creates a draft GitHub release with auto-generated notes. +# Nothing is built here. When you publish the draft release, package.yml builds and attaches +# the binaries and publish-pypi.yml uploads to PyPI. on: push: @@ -59,9 +58,3 @@ jobs: --title "$TAG" \ --generate-notes \ --notes-start-tag "$LAST_TAG" - - - name: Trigger binary builds - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.version.outputs.tag }} - run: gh workflow run package.yml --ref "$TAG" diff --git a/contributing.md b/contributing.md index 40202ce1..0368dd27 100644 --- a/contributing.md +++ b/contributing.md @@ -179,15 +179,15 @@ To investigate what's packaged in the executable, use https://pyinstxtractor-web ### Publishing a release -Merging into `main` triggers `release-on-merge.yml`, which creates a **draft** release, pushes the version tag, and dispatches `package.yml` to build the platform binaries. +Merging into `main` triggers `release-on-merge.yml`, which pushes the next patch version tag and creates a **draft** release for it. Nothing is built at this point. When you then **publish** that draft release in the GitHub UI: -1. `package.yml` fires again on the `release: published` event and rebuilds the binaries against the exact tagged commit. +1. `package.yml` fires on the `release: published` event and builds the binaries against the tagged commit. 2. Before binaries are attached, the workflow **pauses at the `upload_to_release` job** waiting for a reviewer to approve the `release` environment. Go to **Actions → the latest `Package-and-Upload` run → Review deployments → Approve**. The four binaries (Windows `.exe`, Ubuntu `tabcmd`, macOS x86 and arm64 `.app.tar`) get attached after approval. 3. Separately, `publish-pypi.yml` uploads the wheel to PyPI, gated on the `pypi` environment. -If binaries need to be re-attached to an existing release (e.g. one build leg originally failed), dispatch the workflow at the target tag: +If binaries need to be re-attached to a published release (e.g. one build leg originally failed), dispatch the workflow at the target tag. This doesn't work for a draft release: the upload step looks the release up by tag, and GitHub doesn't return drafts by tag. ```shell gh workflow run package.yml --ref v2.1.0 @@ -200,24 +200,20 @@ Same reviewer gate applies. ## Release process -1. Create a new Github project release manually: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases +Releases are created and published as described in [Publishing a release](#publishing-a-release). The manual steps are: + +1. Edit the draft release created by `release-on-merge.yml` before publishing it: - include a useful list of changes since the last release - include a clear list of remaining non-server-admin functional gaps -1. This will trigger our github packaging action to run on 3 different OSs. +1. Publish the release. This triggers `package.yml` and `publish-pypi.yml`: - write an updated metadata file with the correct version number. - build the python wheel - - run pyinstaller to create executables - - save the executable as an artifact on that job. - -1. Find the artifacts created by this job and manually copy them to the new release. -- manually download. They will all be returned as zips -- unzip the windows.exe and mac.app.tar files and upload those -- do not unzip the linux app, github doesn't like it. upload as tabcmd.zip -(Pay attention to what the file type is, github also sends it as a zip if you download with curl etc. TODO: automate workflow with a github action) + - run pyinstaller to create executables on each OS + - after a reviewer approves the `release` environment, attach the executables to the release. + - upload the wheel to PyPI (after approval of the `pypi` environment). -1. To trigger publishing to pypi run the manual workflow on main with 'pypi'. +1. To publish to PyPI without a release (or to test on Test PyPI), run the manual `Publish to PyPi` workflow on main. 1. When the packages are available on pypi, you can run the 'Pypi smoke test action'. This action will also be run every 24 hours to validate doing pip install. (TODO: automate the after-release trigger) -