Publish an immutable, signed CLI release and update the Homebrew cask. Releases are built by GoReleaser and triggered by pushing a semantic tag.
For how releases are consumed (install, self update, channels), see
../cli-updates.md. This runbook is the producer side.
You want to ship a new CLI version to users (stable or beta).
mainis green and holds exactly the commit you want to ship.- A
codeflybinary new enough to havepublish(it bumps the manifest for you). - You can push tags to
codefly-dev/cli, and a GitHub token with write access to it —GITHUB_TOKEN/GH_TOKEN, orgh auth login.publishopens the release pull request with it.--dry-runrequires it too, so the rehearsal fails on a missing credential rather than the real run. - Release secrets are configured in the repo:
CODEFLY_RELEASE_SIGNING_KEY(its public half must matchpkg/cliupdate/release-signing-cert.pem).
.github/workflows/release.yaml runs GoReleaser inside
ghcr.io/goreleaser/goreleaser-cross (CGO on — tree-sitter needs the platform compilers) and
publishes, per tag:
- 4 archives:
codefly_<version>_{darwin,linux}_{amd64,arm64}.tar.gz checksums.txt+checksums.txt.sig(release-key signature)- one Syft SBOM per archive
- GitHub artifact attestations
- a Homebrew cask in the separate Homebrew tap repo
The version, commit, and UTC build date are injected via ldflags into pkg/cliupdate and
reported by codefly version --json. macOS binaries are Developer ID signed and notarized.
codefly publish is the release. It bumps the manifest, lands the bump on main through a
release pull request, then tags the commit main ends up carrying — in the right order, behind
pre-flight gates — so there is no step to forget:
codefly publish --dry-run # show the version it would cut; changes nothing
codefly publish # patch bump; or: codefly publish minor|major
codefly publish beta # next beta: 1.4.0 → 1.4.1-beta.1, then beta.2It auto-detects this repo from pkg/cli/info.yaml and refuses to do anything unless the
working tree is clean, you are on main, it exactly matches origin/main, and the target tag
does not already exist. Neither main nor the tag is ever force-pushed.
The bump lands like any other change. publish pushes the release commit to a short-lived
release-x.y.z branch, opens a release: vx.y.z pull request against main, waits for
main's required checks (bootstrap-audit, control-integration, coverage, race,
dashboard), squash-merges it, and only then tags the commit main actually carries and
pushes that tag. Tags are outside branch protection, so the tag push stays direct.
Mergeability is not CI evidence: before merging, publication requires successful checks and commit statuses on the exact PR head, and refuses failed or cancelled checks even when GitHub allows the merge. Missing or pending checks keep waiting. Before pushing a tag it also requires successful CI on the actual tag commit, including when finishing an untagged release already on main.
That is what lets main enforce its checks on every account (enforce_admins: true).
Pushing a freshly-created release commit straight to main can never satisfy a required check
— the commit has no check results — so a direct-push release forces protection to exempt
somebody, and in a fleet where every agent pushes as the same admin, exempting admins exempts
everyone.
Failure handling:
- Before the merge — the manifest is restored, the local release commit removed, and the release branch deleted from origin. Nothing shipped.
- Red pull request —
publishnames the failing checks and aborts as above rather than waiting out its budget (45 minutes). - After the merge, before the tag — the one non-atomic window. The bump is on
mainwith no release.publishsays so; re-runcodefly publishand it recognizes the untaggedrelease:commit onmainand finishes that release instead of bumping again. Do not bump past it — that burns a version. The re-run rebuilds whatever the release uploads first, so an agent resumed this way ships its loader archives rather than an empty release. - After the tag — the release commit and tag are immutable and remain even if the release workflow later fails.
It also reconciles the manifest against the tags actually on origin: if the manifest has
drifted behind (as it had at 0.1.145 while tags ran to v0.1.150), it bumps from the
latest tag rather than colliding with an existing one.
Do not hand-roll git tag. The workflow's first gate requires the tag to equal v +
pkg/cli/info.yaml's version, and a hand-cut tag skips the bump: the gate rejects it about a
second in, nothing is published, and the version number is burned. Tags v0.1.146 through
v0.1.150 were all lost that way.
gh run watch --workflow release.yamlThe workflow, in order: checks out the tag → sets up Go from go.mod → downloads Syft →
qualifies the clean tag (tag == v + pkg/cli/info.yaml, tag points at this commit, tree
clean, snapshot build produces all 4 archives) → loads and verifies the signing key against
pkg/cliupdate/release-signing-cert.pem → runs goreleaser release --clean in the cross image
→ attests the archives/SBOMs → verifies the published release (immutability, asset
presence, signature) → verifies the Homebrew cask via .github/scripts/verify-homebrew-cask.sh.
Pushing a v*.*.* tag is the only trigger; there is no manual "release" button to click.
Concurrency is serialized per ref and not auto-cancelled.
gh release view v1.4.0
brew update && brew install --cask codefly-dev/cli/codefly # or `brew upgrade --cask`
codefly version --json # version/commit/buildDate match the tag
codefly self check-update # sees the new stableQualify the clean tagfails in seconds — the tag does not matchpkg/cli/info.yaml, i.e. the tag was cut by hand instead of bycodefly publish. Nothing was published, so there is no release to supersede; runcodefly publishand let it cut the next tag.- Signing-key mismatch — the key fingerprint must equal the cert's public-key fingerprint. The workflow fails fast on this; fix the secret, don't re-tag.
- A tag whose release workflow failed — the tag stays on origin and its version is burned.
Land the fix through a pull request and run
codefly publishagain; it cuts the next version. That release's agent compatibility record (tools/agentcontract) compares against the newest stable tag that has a published release, passing over tags that have none. - Re-running a tag — a release is immutable. To ship a fix, cut a new tag (e.g.
v1.4.1); do not force-move a published tag. - Homebrew verification failed but the GitHub release exists — the cask lives in a separate repo; check that repo's write token/permissions. The GitHub release can be valid while the cask step lags.
-
maingreen, at the exact commit to ship -
codefly publish --dry-runshows the version you expect -
codefly publishran clean (release PR merged green,release:commit onmain, tag pushed) -
release.yamlgreen end to end -
codefly version --jsonreports the tag -
brewinstall/upgrade works;self check-updatesees it
The October 2026 refresh selects golang.org/x/net 0.62.0 for
GO-2026-6860 and GO-2026-6866, and gRPC 1.85.0 supersedes the old 1.84.0
hold. The dependency update command's automatic outdated list contains direct
dependencies; verify indirect dependencies such as x/net explicitly too.
Use codefly update deps --dependency golang.org/x/net@v0.62.0 to select an
existing direct or indirect external requirement without moving unrelated
versions. The repeatable flag validates every selection across the selected
modules before mutation, then uses the same Go resolver, tidy and audit path as
the automatic update. Modules without the selected requirement are not changed;
unknown or duplicate selections and first-party selections are refused. Exact
selections also retain a version required by a compatibility gate without
silently undoing that hold during an unrelated security update.
Companion publication compiles this checkout into the images. Merge the CLI
fix before dispatching companions.yaml against Core's exact merged commit
with new companion versions. A CLI merge does not refresh existing images:
inspect and scan the published executable, record each index digest, and
rebuild the services consuming it. Do not overwrite previously published tags.