diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 9413ed6a..7251e3e5 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -9,15 +9,21 @@ 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: - - '*' + 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 || github.ref_name }} + cancel-in-progress: false + 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 +103,93 @@ 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 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 + # release upload where e.g. macOS is missing is never possible. + upload_to_release: + name: Attach build artifacts to release + needs: buildexe + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + steps: + # v7 to match upload-artifact@v7; bump both together + - name: Download all build artifacts + uses: actions/download-artifact@v7 + 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. + - 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: ${{ steps.tag.outputs.tag }} + 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: ${{ steps.tag.outputs.tag }} + 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: ${{ steps.tag.outputs.tag }} + 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: ${{ steps.tag.outputs.tag }} overwrite: true 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