From 4f0fb0bf121b72780e9355eee7d83e382a26fca3 Mon Sep 17 00:00:00 2001 From: Myles Anderson <135627999+myles332@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:19:48 -0700 Subject: [PATCH] fix(release): attach macOS DMG to releases (repo var + PAT cascade + manual dispatch) (#186) The signed macOS DMG never made it onto releases, so releases/latest/download/OpenResearch.dmg 404s. Two root causes: 1. The cheap gate reads `vars.MACOS_SIGNING_ENABLED`, which only sees repo/org variables, but the variable was created at environment scope. Fixed out-of-band by adding the repo-level variable. 2. `release-on-bump` dispatches `release.yml` with GITHUB_TOKEN, so GitHub's anti-recursion rule suppresses the `workflow_run` cascade and the macOS attach never runs for real releases (only PR dry-runs, which correctly skip). The workflow's comment claiming `workflow_run` fires regardless of the upstream author was wrong. Changes: - release-on-bump.yml: dispatch release.yml with a PAT (RELEASE_DISPATCH_TOKEN) so the Release run is owned by a real token and its completion cascades to the attach. Falls back to GITHUB_TOKEN when the PAT is missing or invalid so a bad PAT never blocks a release (the DMG is then a manual dispatch). - release-macos-app.yml: add a `workflow_dispatch` (tag input) trigger as the manual/recovery path; the gate and checkout handle both trigger types. This is how releases cut before the pipeline worked get their DMG. - DISTRIBUTION.md: document the repo-variable scope, the PAT secret and rotation, the manual re-attach command, and the required-reviewer gate. Co-authored-by: Claude Opus 4.8 --- .github/workflows/release-macos-app.yml | 54 ++++++++++++++++--------- .github/workflows/release-on-bump.yml | 30 +++++++++++--- macos/DISTRIBUTION.md | 32 +++++++++++++-- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release-macos-app.yml b/.github/workflows/release-macos-app.yml index 911afc1e..c9da5cc5 100644 --- a/.github/workflows/release-macos-app.yml +++ b/.github/workflows/release-macos-app.yml @@ -1,14 +1,15 @@ -# Attaches the signed, notarized OpenResearch.app (as a DMG) to each GitHub -# Release. cargo-dist's "Release" workflow (release.yml) creates the Release with -# the default GITHUB_TOKEN, and GitHub's anti-recursion rule blocks a -# `release: published` event from a GITHUB_TOKEN action from triggering other -# workflows (the same constraint release-on-bump.yml documents). So we trigger on -# the Release workflow *completing* via `workflow_run`, which fires regardless of -# what authored the upstream run. +# Attaches the signed, notarized OpenResearch.app (as a DMG) to a GitHub Release. # -# release.yml also runs on pull_request (a dry-run that publishes nothing), so we -# only proceed for a successful `workflow_dispatch` run (a real release) and then -# confirm the tag's release actually exists before uploading. +# Two triggers: +# - workflow_run: fires when the "Release" workflow (release.yml) completes — +# the automatic path. IMPORTANT: GitHub suppresses workflow_run when the +# upstream run was authored by GITHUB_TOKEN (its anti-recursion rule), so +# release.yml must be dispatched by a PAT (see release-on-bump.yml) for this +# to fire on a real release. release.yml also runs on pull_request (a dry-run +# that publishes nothing), so we only proceed for a successful +# `workflow_dispatch` run and then confirm the tag's release actually exists. +# - workflow_dispatch: manually re-attach the DMG for a given tag — the recovery +# path, and how a release cut before this pipeline worked gets its DMG. # # Signing secrets live in the `release-signing` environment (not repo secrets), # so only the reviewed, environment-gated `macos-app` job can read them. The @@ -23,6 +24,12 @@ on: workflow_run: workflows: ["Release"] types: [completed] + workflow_dispatch: + inputs: + tag: + description: Release tag to attach the macOS app to (e.g. v0.1.99) + required: true + type: string permissions: contents: write # upload release assets @@ -32,12 +39,13 @@ concurrency: cancel-in-progress: false jobs: - # Cheap ubuntu gate: only a real, successful release with signing enabled - # (the non-secret MACOS_SIGNING_ENABLED variable) and a Release that actually - # exists — so a macOS runner (billed 10x) never boots otherwise, and no secret - # is ever read here. Resolves the tag from the released commit. + # Cheap ubuntu gate: only proceed for signing-enabled runs that map to a real + # release — an automatic workflow_run from a successful workflow_dispatch + # Release, or a manual workflow_dispatch of this workflow. Keys on the + # non-secret MACOS_SIGNING_ENABLED variable and confirms the release exists, so + # a macOS runner (billed 10x) never boots otherwise and no secret is read here. check: - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'workflow_dispatch' && vars.MACOS_SIGNING_ENABLED == 'true' }} + if: ${{ vars.MACOS_SIGNING_ENABLED == 'true' && (github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'workflow_dispatch')) }} runs-on: ubuntu-latest outputs: ok: ${{ steps.gate.outputs.ok }} @@ -47,12 +55,20 @@ jobs: steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ github.event.workflow_run.head_sha || inputs.tag }} - id: gate + env: + INPUT_TAG: ${{ inputs.tag }} run: | set -euo pipefail - version="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')" - tag="v${version}" + # Manual dispatch names the tag; the automatic path derives it from the + # released commit's Cargo.toml. + if [ -n "$INPUT_TAG" ]; then + tag="$INPUT_TAG" + else + version="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')" + tag="v${version}" + fi if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "::notice::No release $tag found (dry-run dispatch?) — nothing to attach." echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0 @@ -72,7 +88,7 @@ jobs: steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ github.event.workflow_run.head_sha || inputs.tag }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: diff --git a/.github/workflows/release-on-bump.yml b/.github/workflows/release-on-bump.yml index e4480b30..00241c63 100644 --- a/.github/workflows/release-on-bump.yml +++ b/.github/workflows/release-on-bump.yml @@ -10,8 +10,16 @@ # GITHUB_TOKEN don't trigger other workflows (GitHub's anti-recursion rule), # so a GITHUB_TOKEN-pushed tag would never start release.yml. workflow_dispatch # is the documented exception ("workflow_dispatch and repository_dispatch -# events always create workflow runs"), so the default token is sufficient and -# no PAT/secret is needed. +# events always create workflow runs"). +# +# Why a PAT (RELEASE_DISPATCH_TOKEN) and not the default GITHUB_TOKEN: the +# Release run's *completion* must cascade to release-macos-app.yml via +# workflow_run, and GitHub applies the same anti-recursion rule there — a run +# authored by GITHUB_TOKEN does not fire workflow_run. Dispatching with a PAT +# makes the Release run owned by a real token, so its completion triggers the +# macOS attach. The PAT needs Actions: read and write (to dispatch release.yml). +# If the secret is missing or invalid, releases still publish but the DMG must be +# attached manually via release-macos-app.yml's workflow_dispatch. See DISTRIBUTION.md. # # This never increments a version: the version is always a human decision made # in a reviewed PR, and "merge a PR that bumps Cargo.toml" is the release act. @@ -29,7 +37,7 @@ concurrency: permissions: contents: read - actions: write # to dispatch release.yml + actions: write # github.token fallback path dispatches release.yml jobs: dispatch: @@ -40,7 +48,12 @@ jobs: - name: Dispatch the release workflow env: - GH_TOKEN: ${{ github.token }} + # Dispatching with the PAT makes the Release run cascade to the macOS + # attach (see header). GITHUB_TOKEN is the fallback so a missing OR + # invalid/expired PAT never blocks a release — it just ships without + # the auto-attach (recover via release-macos-app.yml's workflow_dispatch). + DISPATCH_PAT: ${{ secrets.RELEASE_DISPATCH_TOKEN }} + FALLBACK_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -56,6 +69,11 @@ jobs: fi echo "Dispatching release.yml with tag=${tag} (version is unreleased)." - gh workflow run release.yml --ref main -f tag="${tag}" - echo "Dispatched. The release will appear at:" + if [ -n "$DISPATCH_PAT" ] && GH_TOKEN="$DISPATCH_PAT" gh workflow run release.yml --ref main -f tag="${tag}"; then + echo "Dispatched with RELEASE_DISPATCH_TOKEN — the macOS DMG will attach automatically." + else + [ -n "$DISPATCH_PAT" ] && echo "::warning::RELEASE_DISPATCH_TOKEN failed; used GITHUB_TOKEN. Release ships but the DMG won't auto-attach — run release-macos-app.yml manually for ${tag}." + GH_TOKEN="$FALLBACK_TOKEN" gh workflow run release.yml --ref main -f tag="${tag}" + fi + echo "The release will appear at:" echo " https://github.com/${GITHUB_REPOSITORY}/actions/workflows/release.yml" diff --git a/macos/DISTRIBUTION.md b/macos/DISTRIBUTION.md index 0d2501cd..d014a36f 100644 --- a/macos/DISTRIBUTION.md +++ b/macos/DISTRIBUTION.md @@ -2,14 +2,32 @@ `scripts/build-macos-app.sh` builds the app; `scripts/package-macos-app.sh` signs, notarizes, and packages it into a DMG. CI -(`.github/workflows/release-macos-app.yml`) runs both after each release and -attaches `OpenResearch.dmg`: +(`.github/workflows/release-macos-app.yml`) runs both after a release (see the +trigger caveat below) and attaches `OpenResearch.dmg`: ``` https://github.com/alphaXiv/openresearch-cli/releases/latest/download/OpenResearch.dmg ``` -The release job is a no-op until the `MACOS_SIGNING_ENABLED` variable is `true`. +The release job is a no-op until the **repository** variable +`MACOS_SIGNING_ENABLED` is `true` (a repo variable, not an environment one — the +cheap gate job has no `environment:` and only sees repo/org variables). + +The attach runs automatically only when the `Release` workflow was dispatched by +a PAT (see below); GitHub suppresses the `workflow_run` cascade for runs authored +by `GITHUB_TOKEN`. To attach a DMG to a release that missed it, run the **Attach +macOS app to release** workflow manually (Actions → Run workflow → enter the tag, +e.g. `v0.1.99`), or: + +```bash +gh workflow run release-macos-app.yml -f tag=v0.1.99 +``` + +The manual path checks out `inputs.tag` and runs the signing scripts under the +`release-signing` environment, so the **required reviewer approving the run is +the real gate on the certificate** — verify the tag points at trusted code (the +`main`-only deployment-branch restriction covers the workflow file, not the +checked-out tag). ## Configure signing (CI) @@ -32,6 +50,14 @@ From it you produce the six values below. 2. Set repo **variable** `MACOS_SIGNING_ENABLED = true` to switch the pipeline on. +3. Add repo **secret** `RELEASE_DISPATCH_TOKEN` — a fine-grained PAT scoped to + this repo with **Actions: read and write** (classic: `repo` + `workflow`). + `release-on-bump.yml` dispatches `release.yml` with it so the Release run is + owned by a real token and its completion cascades to the macOS attach. If it's + missing or invalid, releases still ship but the DMG is a manual dispatch. + Fine-grained PATs expire (≤1yr) — rotate it before then, or releases keep + shipping without the auto-attach until it's renewed. + Also enable **Require a pull request** + **Require review from Code Owners** on `main` (see `.github/CODEOWNERS`) so the signing scripts can't change unreviewed. Never commit the `.p12`.