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 <noreply@anthropic.com>
This commit is contained in:
Myles Anderson
2026-08-12 17:19:48 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7edd229964
commit 4f0fb0bf12
3 changed files with 88 additions and 28 deletions
+35 -19
View File
@@ -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:
+24 -6
View File
@@ -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"
+29 -3
View File
@@ -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`.