Release Candidate Promotion
Decision
Fairway builds, signs, notarizes, tests, and packages a release candidate before the final remote tag exists. The final tag promotes those exact immutable candidate assets; it does not rebuild them.
This separates two concerns:
- Candidate qualification proves that an exact pushed
maincommit can produce the complete release packet under the production signing, notarization, SBOM, license, vulnerability, and assurance controls. - Release promotion binds an annotated semantic-version tag to one successful rehearsal run and creates a draft release from that run's exact assets.
Candidate identity
The manual Release Rehearsal workflow is dispatched with:
- semantic version in
vX.Y.Zform; - exact lowercase 40-character source SHA;
- source SHA equal to the selected
mainworkflow-dispatch head; - no existing remote tag for the requested version.
The workflow checks out that SHA, creates a local-only annotated tag so GoReleaser produces final names and embedded version metadata, then runs the same signing, notarization, test, assurance, and packaging path used for a release.
Promotion packet
The uploaded artifact contains exactly:
- four platform archives;
- one GoReleaser checksum file;
- one signed release-assurance archive;
- one portable assurance checksum;
rehearsal.json.
rehearsal.json uses schema fairway.release-rehearsal.v1 and binds the exact
version, source SHA, workflow builder identity, policy version, creation time,
asset names, byte sizes, and SHA-256 digests. Creation and verification reject
missing files, extra files, symlinks, malformed identity, checksum drift, and
asset mutation.
The workflow also extracts the macOS arm64 archive, checks its embedded Fairway
version, and runs strict native codesign verification before uploading the
packet.
Signing, notarization, and assurance private-key secrets are scoped only to the steps that require them. Tests, checkout, setup, smoke, packet assembly, and artifact upload do not receive the raw secrets. The imported signing keychain is locked and its temporary import files are removed immediately after the GoReleaser build.
Promotion binding
The release owner creates one annotated tag whose message includes:
fairway-rehearsal-run: <github-actions-run-id>
The tag-triggered workflow:
- requires an annotated tag and exactly one rehearsal run id;
- resolves the tag's commit;
- requires the referenced run to be a successful
workflow_dispatchof.github/workflows/release-rehearsal.ymlonmainat that exact commit; - downloads exactly one unexpired artifact named for the version and SHA;
- verifies the rehearsal manifest and safely extracts the assurance archive with bounded path, type, symlink, duplicate, and size checks;
- verifies signed assurance against expected version, source, builder, policy, and pinned public key;
- uploads a verified promotion input for a separate write-authorized job;
- creates a draft GitHub release from the packet's exact files.
Candidate verification runs with read-only repository permissions and checkout
does not persist credentials. The separate promotion job receives
contents: write only after verification and does not check out or execute
candidate code. The workflow has no signing or notarization secrets and does
not run GoReleaser. This is the enforcement point for "build once, promote
exactly."
Failure and retry behavior
- A failed rehearsal has no tag, release, Homebrew, or deployment side effect.
- A corrected candidate uses a new rehearsal run. The final annotation names only the approved run.
- A failed promotion may be rerun against the same tag and immutable packet when no release asset changed.
- If candidate bytes or source change, run a new rehearsal. Never modify a packet or reuse a published version.
- Rehearsal artifacts expire after 30 days. Promotion after expiry requires a new rehearsal of the same source and version.
Publication of the draft and Homebrew update remain explicit reviewed actions after promotion verification.