Release
Versioning
Semver. Pre-1.0 means breaking changes are allowed in minor versions; document them in the changelog. Prefer correcting the product model over maintaining accidental behavior for the current single controlled adopter. Still preserve or explicitly migrate durable project-owned data and policy; pre-1.0 is not permission for silent loss or downgrade.
v0.x.y— pre-stable. CLI surface and config schema may change inx.v1.0.0— schema and CLI surface frozen. Migrations forward-compatible.
Tags
- Tag format:
vX.Y.Z. Nov0.0.1-rc1shenanigans during week 1. - Tag from
mainonly. - Run the production release rehearsal before creating the remote tag.
- Annotated tags must bind the approved rehearsal run:
git tag -a v0.2.5 -m $'v0.2.5\n\nfairway-rehearsal-run: <run-id>'
See Release Candidate Promotion.
Changelog
CHANGELOG.mdat repo root, kept in Keep a Changelog format.- One section per released version, plus an
Unreleasedsection at the top. - Each PR touches the
Unreleasedsection.
User-facing release highlights
docs/release-highlights.mdcontains the curated GitHub Release highlights for the next tag.- Keep it to at most three bullets, written for readers who do not follow the repository history.
- The release owner drafts the bullets from
docs/release-notes.md,CHANGELOG.md, and the release-run assessment. - Governance approves the wording and ops approves the release workflow posture before tagging.
- The release workflow loads the file into
FAIRWAY_RELEASE_HIGHLIGHTS, and GoReleaser prepends it to the generated changelog under## Highlights. Generated changelog detail remains below## Changelog.
Release preparation split
Fairway release work is split into separate tracked tasks so preparation, dashboard lifecycle validation, and publishing do not blur together:
- Release preparation updates release notes, highlights, distribution posture, and the release-readiness assessment. It may name the candidate version and source SHA, but it does not tag, publish, restart dashboards, or declare the release complete.
- Dashboard lifecycle/version-readback work verifies the restart commands and status/version surfaces operators need before a shared dashboard is restarted with a newly built binary.
- Release publishing cuts the reviewed tag, builds and smoke-verifies artifacts, publishes or stages documentation release content, restarts the configured dashboards when authorized, and records version/readback evidence.
Use this split for release tasks such as FW-218, FW-219, and FW-220. A clean release-prep assessment is evidence for publish readiness, not a substitute for the publish task.
goreleaser
- Config at
.goreleaser.yaml. - CI runs
goreleaser checkso release config drift is caught before tags. - Targets:
darwin/amd64,darwin/arm64,linux/amd64,linux/arm64. - Archives:
fairway_<version>_<os>_<arch>.tar.gz. - Checksums:
fairway_<version>_checksums.txt. - GitHub Release drafts are created from
fairway-run/fairwayonly after the signed release assurance bundle verifies the exact candidate archives. - The macOS CLI binaries are signed with native
codesignand submitted with nativenotarytoolfrom the macOS release runner before Homebrew publishing. GoReleaser OSS still drives the signed candidate build, archives, and checksums. Fairway gates the exact archives before GitHub draft creation; the reviewed publish step updates the cask from those verified digests. GoReleaser Pro is not required unless Fairway later ships macOS app bundles, DMGs, or PKGs.
CI release flow
- Dispatch
Release Rehearsalwith the proposed version and exact pushedmainSHA. - The rehearsal runs tests, GoReleaser, signing, notarization, candidate binary smoke, and signed assurance verification before uploading one immutable promotion packet.
- Create and push an annotated tag containing the successful rehearsal run id.
- The tag workflow verifies the run identity and exact packet, then creates a GitHub draft from those assets without rebuilding.
- The separately reviewed publish step makes the draft public and updates the Homebrew cask from the verified archive digests.
- No automatic publishing to other package registries until v1.0.
GitHub Actions runtime posture
Fairway workflows use Node 24-capable GitHub Action majors to avoid Node.js 20 runtime deprecation warnings:
actions/checkout@v5actions/setup-go@v6actions/setup-node@v5actions/upload-artifact@v6goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94(v7.2.3)
GoReleaser action invocations are commit-pinned because the rehearsal build step receives notarization authority. Updating the action requires an explicit review of the new upstream commit before changing the pin.
The release workflow must pass goreleaser check before the next tag. The Docs
Portal workflow must still run npm ci and npm run build; the deploy step is
validated on pushed main with the Cloudflare secrets present. Hosted GitHub
runners satisfy the required action runner version; self-hosted runners must be
kept current enough for Node 24 action runtimes before using these workflow
versions.
Release repositories
- Source/release repo:
fairway-run/fairway. - Homebrew tap repo:
fairway-run/homebrew-tap. - Tap command:
brew tap fairway-run/tap. - Install command:
brew install --cask fairway.
The tap repository is separate from the source repository. The release workflow
must use a dedicated tap token; the default GITHUB_TOKEN is not enough for
cross-repository tap updates.
Before the first release, initialize the tap repository with a main branch so
GoReleaser can push the first generated cask:
tmpdir=$(mktemp -d /tmp/fairway-homebrew-tap.XXXXXX)
cd "$tmpdir"
git init
git branch -M main
mkdir -p Casks
touch Casks/.gitkeep
cat > README.md <<'EOF'
# Homebrew Tap for Fairway
brew tap fairway-run/tap
brew install --cask fairway
EOF
git add README.md Casks/.gitkeep
git commit -m "Initialize Homebrew tap"
git remote add origin git@github.com:fairway-run/homebrew-tap.git
git push -u origin main
Required release secrets
Set these GitHub Actions secrets on fairway-run/fairway before cutting a
release tag:
| Secret | Purpose |
|---|---|
HOMEBREW_TAP_GITHUB_TOKEN | Fine-scoped token with write access to fairway-run/homebrew-tap. |
MACOS_SIGN_P12 | Base64-encoded Developer ID Application .p12 certificate. |
MACOS_SIGN_PASSWORD | Password for the .p12 certificate. |
MACOS_CODESIGN_IDENTITY | Developer ID Application identity fingerprint or exact name used by native codesign. |
MACOS_NOTARY_KEY | Base64-encoded App Store Connect .p8 API key. |
MACOS_NOTARY_KEY_ID | App Store Connect API key id. |
MACOS_NOTARY_ISSUER_ID | App Store Connect issuer id. |
FAIRWAY_RELEASE_ASSURANCE_SIGNING_KEY | Private Ed25519 key used only while building signed release assurance. |
Set FAIRWAY_RELEASE_ASSURANCE_PUBLIC_KEY as a separately pinned GitHub
Actions variable. Promotion verifies assurance with that public key and does
not receive the private signing key.
For local/manual signing experiments, .apple-app-specific.env may hold local
Apple credential material. It is ignored by git and must never be committed,
printed in logs, pasted into task evidence, or shared with any consumer
project's domain or deployment credentials. CI should use the App Store Connect
API key secrets above.
macOS signing and notarization baseline
Fairway uses Developer ID signing and native Apple notarization for macOS CLI artifacts. The current release baseline is:
- Certificate type:
Developer ID Application. - Certificate chain:
Developer ID Application -> Developer ID Certification Authority -> Apple Root CA. - Team identifier: Apple Developer team id for the Fairway release account.
- Hardened runtime: enabled by the release hook with
codesign --options runtime. - Notarization auth: App Store Connect API key, not Apple ID password auth.
GoReleaser Pro is only needed if Fairway later ships native app bundles, macOS DMGs, or macOS PKGs. The first release distributes a CLI binary, so the OSS release path plus native macOS build hooks is sufficient.
For the first public release, use the working Developer ID certificate from
the previous Sub-CA. The G2 certificate is available, but local validation showed
that codesign will not build the G2 chain until the Developer ID G2
intermediate is trusted from the System chain. Previous Sub-CA certificates
created after February 1, 2022 expire on February 1, 2027; switch to G2 well
before that date and require a passing local/CI codesign --verify --strict
check before publishing.
Local release material lives under ignored paths only:
| Local path | Purpose |
|---|---|
.release-certs/developerID_application_identities.p12 | Local Developer ID signing bundle. |
.release-certs/developerID_application_identities.p12.base64 | Value source for MACOS_SIGN_P12. |
.release-certs/macos-sign-p12-password.local | Value source for MACOS_SIGN_PASSWORD. |
.release-certs/AuthKey_<KEY_ID>.p8 | App Store Connect notary API key. |
.release-certs/AuthKey_<KEY_ID>.p8.base64 | Value source for MACOS_NOTARY_KEY. |
.apple-app-specific.env | Local-only Apple release environment values. |
Back up these files outside git, for example under the operator's private
iCloud project folder. Do not place certificate passwords, private keys, API
keys, issuer ids, or key ids in public docs or task evidence. Do not use
dist/certs/ for durable release credentials because GoReleaser cleans
dist/.
Set release secrets without echoing values:
gh secret set MACOS_SIGN_P12 \
--repo fairway-run/fairway \
< .release-certs/developerID_application_identities.p12.base64
gh secret set MACOS_SIGN_PASSWORD \
--repo fairway-run/fairway \
< .release-certs/macos-sign-p12-password.local
gh secret set MACOS_CODESIGN_IDENTITY \
--repo fairway-run/fairway \
--body "$MACOS_CODESIGN_IDENTITY"
gh secret set MACOS_NOTARY_KEY \
--repo fairway-run/fairway \
< .release-certs/AuthKey_<KEY_ID>.p8.base64
gh secret set MACOS_NOTARY_KEY_ID \
--repo fairway-run/fairway \
--body "$MACOS_NOTARY_KEY_ID"
gh secret set MACOS_NOTARY_ISSUER_ID \
--repo fairway-run/fairway \
--body "$MACOS_NOTARY_ISSUER_ID"
gh secret set HOMEBREW_TAP_GITHUB_TOKEN \
--repo fairway-run/fairway
Local signing smoke:
tmpdir=$(mktemp -d /tmp/fairway-sign-test.XXXXXX)
go build -o "$tmpdir/fairway" ./cmd/fairway
codesign --force --timestamp --options runtime \
--sign "<Developer ID Application identity>" \
"$tmpdir/fairway"
codesign --verify --strict --verbose=4 "$tmpdir/fairway"
codesign -dv --verbose=4 "$tmpdir/fairway"
For archive notarization smoke tests, sign the binary, zip the artifact, then
submit the zip to notarytool with the App Store Connect API key:
xcrun notarytool submit fairway.zip \
--key .release-certs/AuthKey_<KEY_ID>.p8 \
--key-id "$MACOS_NOTARY_KEY_ID" \
--issuer "$MACOS_NOTARY_ISSUER_ID" \
--wait
stapler cannot staple a zip archive. For the current tar/zip CLI
distribution, an accepted notarization result is the expected release signal.
Use a .pkg or .dmg if Fairway later needs stapled offline verification.
First Homebrew publish
Run these checks before tagging:
git status --short
go test ./...
go vet ./...
goreleaser check
(cd website && npm run build)
go run ./cmd/fairway workflow check \
--mode deploy \
--require-clean \
--require-pushed
Create one release-run task and packet for each meaningful release attempt:
fairway packet release-run FW-REL-012 \
--version v0.1.2 \
--tag v0.1.2 \
--source-sha "$(git rev-parse HEAD)" \
--release-notes docs/release-notes.md \
--changelog-state "CHANGELOG.md has v0.1.2 section" \
--ci-status pass \
--docs-status pass \
--signing-status pass \
--notary-status pass \
--release-url "https://github.com/fairway-run/fairway/releases/tag/v0.1.2" \
--homebrew-tap-commit <tap-commit-sha> \
--verification-command "go test ./..." \
--verification-command "go vet ./..." \
--verification-command "goreleaser check" \
--verification-command "brew fetch --cask --force fairway-run/tap/fairway"
Generate a Fairway provenance bundle before release review. The bundle should name the release source SHA, included Fairway task ids, evidence summaries, review gates, CI/CD checks, and known waivers, without embedding raw prompts, private transcripts, raw tool bodies, generated-content dumps, credentials, or secrets:
fairway provenance report --since 168h --format json > artifacts/fairway-provenance-v0.1.2.json
fairway provenance report --since 168h --format markdown > artifacts/fairway-provenance-v0.1.2.md
The JSON bundle can be attached to release review evidence, archived with the release packet, or referenced from release notes. Public release notes may link to a reviewed public bundle or summarize its digest/reference, but they should not expose internal-only URLs or sensitive artifact paths. This is the Fairway control-plane input for future SLSA or in-toto attestations; it does not require or replace those systems in the first implementation.
Push the clean candidate commit, then dispatch the production rehearsal before creating a remote tag:
git fetch --all --tags
git status --short --branch
source_sha=$(git rev-parse HEAD)
gh workflow run release-rehearsal.yml \
--repo fairway-run/fairway \
--ref main \
-f version=v0.2.5 \
-f source_sha="$source_sha"
gh run list \
--repo fairway-run/fairway \
--workflow release-rehearsal.yml \
--limit 5
After the rehearsal succeeds, use its numeric run id in the annotated tag. The tag promotion workflow rejects lightweight tags, missing or duplicate run bindings, runs for another SHA, and expired or mutated candidate packets:
rehearsal_run=<successful-run-id>
git tag -a v0.2.5 \
-m $'v0.2.5\n\nfairway-rehearsal-run: '"$rehearsal_run"
git push fairway-run v0.2.5
gh run list --repo fairway-run/fairway --workflow release.yml --limit 5
If the tag-push workflow fails before publication because the runner cannot
resolve remote tag metadata, fix the verifier on main without moving or
recreating the immutable tag. Then dispatch the recovery path against the
existing tag:
gh workflow run release.yml \
--repo fairway-run/fairway \
--ref main \
-f version=v0.2.5
The recovery workflow checks out the existing tag target, verifies the remote annotated tag object, source commit, rehearsal binding, candidate packet, and signed assurance, and revalidates the tag object and commit again immediately before draft creation. It must not accept a lightweight, moved, recreated, or missing tag and must never rewrite the tag as part of recovery.
The GitHub Release is intentionally created as a draft. Review artifacts, checksums, signing/notarization logs, and the proposed Homebrew cask digests before publishing the draft and updating the tap.
Do not treat the release as Homebrew-ready while the GitHub Release is still a
draft. The assurance-gated workflow intentionally does not mutate the Homebrew
tap. The reviewed publish step updates the cask only after the draft and its
verified assets have been inspected. Cask URLs point at
releases/download/vX.Y.Z/... and return 404 until the release is public; the
release-run checklist should classify a premature cask update as failed
verification:
Homebrew cask version == tag, but GitHub release is draft or asset URL is 404.
Action: publish the reviewed release draft, then verify asset URLs and brew fetch.
Verify Homebrew after the cask update lands:
brew untap fairway-run/tap || true
brew tap fairway-run/tap
brew install --cask fairway
fairway help
brew uninstall --cask fairway
For a lower-impact verification that does not install Fairway, update the tap cache and fetch the cask:
tapdir=$(brew --repository fairway-run/tap)
git -C "$tapdir" fetch origin main --prune
git -C "$tapdir" reset --hard origin/main
brew info --cask fairway-run/tap/fairway
brew fetch --cask --force fairway-run/tap/fairway
The expected result is:
- GitHub release is not draft.
- Release assets include checksums and all target archives.
- Asset URLs return 200 after redirects.
- Homebrew cask reports the new version.
brew fetch --cask --force fairway-run/tap/fairwaysucceeds.
Record the observed release state with the release verification guard:
fairway release verify \
--version v0.1.2 \
--tag v0.1.2 \
--source-sha "$(git rev-parse HEAD)" \
--release-notes docs/release-notes.md \
--changelog CHANGELOG.md \
--ci-status pass \
--docs-status pass \
--signing-status pass \
--notary-status pass \
--release-state public \
--release-url "https://github.com/fairway-run/fairway/releases/tag/v0.1.2" \
--asset "https://github.com/fairway-run/fairway/releases/download/v0.1.2/fairway_v0.1.2_checksums.txt=200" \
--homebrew-version 0.1.2 \
--homebrew-tap-commit <tap-commit-sha> \
--brew-fetch-status pass \
--provenance-bundle artifacts/fairway-provenance-v0.1.2.json \
--verification-command "brew fetch --cask --force fairway-run/tap/fairway"
fairway release verify warns when no provenance bundle is named. If
--provenance-bundle is provided, the path must exist; otherwise release
verification fails. The guard also warns when the bundle does not mention the
release version or supplied source SHA, so reviewers can catch stale provenance
before tagging or publishing.
Git tags include the v prefix (v0.1.2), while Homebrew cask versions
normally omit it (0.1.2). fairway release verify normalizes that prefix for
the Homebrew comparison but still fails real semantic mismatches.
The v0.1.2 lesson is explicit: if --release-state draft while
--homebrew-version matches the release tag after prefix normalization,
fairway release verify fails.
Publish the reviewed GitHub release draft first, then verify asset URLs and
brew fetch before treating the Homebrew cask as usable.
If the cask publish fails, fix the tap or release config and rerun the release workflow only when the generated cask will point to the same immutable tag and checksums. If the released artifact itself is wrong, yank and cut a new version; never reuse a version number.
Sovereign release assurance bundle
The manually dispatched release-rehearsal workflow first runs GoReleaser with
--skip=publish, then builds and verifies one
fairway.release-assurance-manifest.v1 package over those exact candidate
archives. Repository configuration must provide secret
FAIRWAY_RELEASE_ASSURANCE_SIGNING_KEY, separately pinned variable
FAIRWAY_RELEASE_ASSURANCE_PUBLIC_KEY, and policy identifier
FAIRWAY_RELEASE_POLICY_VERSION.
The workflow pins the official Anchore Syft download action v0.24.0 by commit
with Syft v1.46.0, plus go-licenses v1.6.0 and govulncheck v1.1.4. It
records build environment/tool versions and fails if required
archives or evidence are absent. A govulncheck finding stops the automatic
no-findings path; it requires a separate reviewed vulnerability disposition
rather than silently publishing an empty VEX.
No tag, GitHub release, or Homebrew side effect occurs when rehearsal fails. After the final annotated tag binds an approved run, the promotion workflow downloads and verifies the exact rehearsal packet and signed assurance before creating a draft release. It does not rebuild, sign, or notarize. A failed assurance or promotion step therefore leaves no public release or tap mutation to clean up. Publishing the draft, updating Homebrew, and authorizing deployment remain separately reviewed actions.
Offline verification requires exact expected version, source, builder, policy, and pinned public key. Measured SLSA fields do not assign a level. Hermeticity and reproducibility remain false until separate repeatable evidence proves them.
Sovereign offline distribution
After current and rollback release-assurance packages verify, an explicitly
reviewed release task may run
scripts/release/build_offline_distribution_bundle.sh. The resulting signed
offline distribution includes both complete assurance packages, all four
supported binary and standalone-verifier targets, local documentation,
configuration and deployment baselines, and fixed verify/install/rollback
scripts. The builder requires the current package source SHA to equal checked
out HEAD; it does not fetch release archives or publish the result.
fairway release offline verify and the standalone
fairway-offline-verify require a separately pinned Ed25519 public key and
exact current and rollback version/source/builder/policy identity. Run the
disconnected lifecycle rehearsal and retain its version, path, backup, state,
digest, rollback, and cleanup evidence before attaching the bundle to a release.
The verifier binary has its own bootstrap trust requirement: obtain it or its approved digest through the customer software-intake boundary before using the copy inside removable media. Bundle verification is not release publication, installation authorization, deployment approval, certification, or risk acceptance. See Sovereign Offline Distribution Bundle.
Restricted security advisory and LTS channel
Security fixes for restricted environments use a separately signed advisory
package that binds affected/fixed versions, severity, mitigations, VEX updates,
support track, rollback identity, and an opaque offline patch bundle. Generate
and pinned-verify the package with fairway security advisory export|verify.
The patch artifact must still pass its own release-assurance and offline-bundle
verification before customer import.
The support and end-of-support policy is canonical in SECURITY.md; operator
steps and trust-root rotation are in
Restricted Advisory and LTS Patch Channel.
Customer acknowledgement is receipt evidence only. No advisory command
publishes, notifies, imports, installs, approves, deploys, accepts risk, or
changes dashboard/public exposure.
Docs portal deployment
The public Fairway docs portal should use separate Cloudflare credentials for
fairway.run. Do not reuse credentials from any consumer project or domain.
Expected secret split:
| Secret | Purpose |
|---|---|
FAIRWAY_CLOUDFLARE_API_TOKEN | Cloudflare Pages/DNS token scoped only to the Fairway zone/project. |
FAIRWAY_CLOUDFLARE_ACCOUNT_ID | Cloudflare account for Fairway Pages deploys. |
FAIRWAY_CLOUDFLARE_ZONE_ID | Zone id for fairway.run. |
FAIRWAY_PAGES_PROJECT | Cloudflare Pages project name. |
Minimum Cloudflare token permissions:
| Scope | Permission | Why |
|---|---|---|
| Account / selected Fairway account | Cloudflare Pages Edit or Pages Write | Create/update Pages projects and publish deployments. |
| Account / selected Fairway account | Account Settings Read | Resolve account metadata during Pages deploy/setup tooling. |
Zone / fairway.run only | DNS Edit or DNS Write | Create/update fairway.run, www.fairway.run, and docs.fairway.run DNS records. |
Zone / fairway.run only | Zone Read | Resolve the zone and validate custom-domain routing. |
If Cloudflare Pages is connected directly to GitHub and Cloudflare owns deploys, the CI token may not need DNS write after initial custom-domain setup. Keep DNS write on a setup/admin token when possible, and use a narrower Pages-only deploy token for routine GitHub Actions deploys.
Local Fairway Cloudflare credentials may be stored in
.env.cloudflare.fairway-run. The file is ignored by git and must not be
printed, committed, or mixed with consumer deployment credentials.
Docs portal edge security
Do not assume Cloudflare Pages alone is the complete security posture. The docs portal setup should explicitly verify the following before the site is treated as production-ready:
- Cloudflare bot protection is enabled for
fairway.runusing the available plan capability, such as Bot Fight Mode, Super Bot Fight Mode, or an equivalent managed bot rule. - Security headers are shipped from the Pages project, normally through a
_headersfile in the static output path:X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-Policywith unused browser features disabledContent-Security-Policyappropriate for the generated Docusaurus assets
- The Pages preview host is not indexed if preview deployments are public. Use
X-Robots-Tag: noindexfor*.pages.devpreview hosts where appropriate. - Custom-domain routing is verified for
fairway.run,www.fairway.run, anddocs.fairway.run. - Any WAF/bot challenge rule is checked against normal docs traffic, GitHub release downloads, Homebrew install paths, and legitimate search crawlers.
Cloudflare setup belongs to its own tracked task because it includes domain, token, static-site, and public-content boundary decisions.
Shared dashboard hostname updates
Shared read-only dashboard hostnames are deployment-owned. Use a neutral Fairway hostname under the deployment owner's domain, while temporarily keeping an older consumer-named hostname as a compatibility alias when migration evidence requires it.
Changing that hostname does not require a GoReleaser or Homebrew cask change as long as Fairway does not embed the public dashboard URL in the binary, archives, cask metadata, or generated release assets. Treat it as a docs/deployment release note item:
- verify the new DNS, tunnel, and Access policy in the deployment-owned task;
- update Fairway docs to prefer the neutral hostname;
- keep the old hostname until replacement reachability and viewer communication are recorded;
- mention the neutral reference hostname in release notes only after the deployment owner is ready to advertise it.
Historical consumer hostname decisions remain in the archived dashboard share plan.
Pre-1.0 distribution
- Homebrew cask for tagged releases:
brew tap fairway-run/tap && brew install --cask fairway. - Direct downloads from GitHub Releases.
- Source install before the first tag:
go install github.com/fairway-run/fairway/cmd/fairway@latest. - Local checkout install:
make install(defaults to~/.local/bin/fairway).
Post-1.0 distribution
- Homebrew cask remains the primary macOS path.
- Possibly Scoop, Nix, AUR — community-contributed.
Yanking a release
A release with a critical bug is yanked by:
- Deleting the tag on the remote (
git push --delete origin vX.Y.Z). - Editing the GitHub Release to "Draft" with a note explaining.
- Cutting
vX.Y.Z+1with the fix.
Never re-use a version number.