Releasing
Releases are cut from dev and published on main. main only ever moves by
fast-forward to a validated dev commit, so both branches share identical,
signed history and no version-bump commit is ever created.
How a release works
Run the Release workflow (.github/workflows/release.yml) from the Actions
tab on the dev branch, or from the command line:
gh workflow run release.yml --ref dev
The workflow then runs these jobs in order:
- Plan checks that
mainis an ancestor of thedevcommit, computes the next version withcog bump --auto --dry-run, and renders the release notes withcog changelog. The version and notes appear in the run summary. - Validate runs the same validation workflow as pull requests against that exact commit: repository checks, the server test suite, and the client test suite.
- Fast-forward main and tag waits for approval through the
releaseenvironment. After approval it pushes the commit tomainwith a plain (non-force) push, which GitHub rejects unless it is a fast-forward, then pushes an annotatedvX.Y.Ztag and creates a draft GitHub release with the cocogitto notes. - Build calls the server, Caddy, and FRPS image workflows and the client
release workflow with the version. Images are pushed to GHCR as
X.Y.Z,X.Y,latest, andsha-<commit>. Client packages and installers are uploaded to the draft release. - Publish release turns the draft into a published release once every build has succeeded.
If the run is rejected or fails before the approval step, nothing has changed.
If a job fails after approval, main and the tag may already exist and the
release stays a draft. Rerun the failed jobs from the same run: the publish job
reuses an existing tag only when it marks the same commit and skips an existing
draft release.
The fast-forward of main is an ordinary push, so workflows triggered by
pushes to main (the E2E report and documentation site) also run.
Versions
Cocogitto derives the version from Conventional Commits since the latest v*
tag, using cog.toml. The version is never committed. Builds receive it as
an input:
- the client gets it from GoReleaser's default
-X main.versionlinker flag and reports it withnixstasis --version; - the server reads
NIXSTASIS_VERSIONinmix.exs, which the server image passes as a Docker build argument; - local
go buildreportsdev, branch CI snapshots report0.0.0-snapshot-<commit>, and non-release server builds report0.0.0-dev.
Branch pushes still build images and client snapshots for verification, but
only the Release workflow publishes images or release artifacts. Manual
workflow_dispatch image builds with push enabled remain available for
one-off testing and are tagged with the branch and commit only.
Repository setup
The workflow depends on this one-time GitHub configuration:
- A
releaseenvironment with yourself (or the maintainers) as required reviewers and deployments limited to thedevbranch. The approval request appears on the run and shows the planned version and notes. - A deploy key with write access whose private key is stored as the
RELEASE_DEPLOY_KEYsecret on thereleaseenvironment, so only approved release jobs can read it. - A branch ruleset on
mainthat blocks direct updates, deletion, and force pushes, with the deploy key as the only bypass actor. - A tag ruleset on
v*that blocks creation, update, and deletion, with the same bypass.
The deploy key's private half exists only in the RELEASE_DEPLOY_KEY
environment secret and is never needed elsewhere. To rotate it, or after a
suspected exposure, delete the deploy key from the repository settings,
generate a new ed25519 key pair, add its public half as a write-enabled deploy
key, and overwrite the secret with the private half:
gh repo deploy-key add release_deploy_key.pub --allow-write \
--title 'Release workflow (main + v* tags)'
gh secret set RELEASE_DEPLOY_KEY --env release < release_deploy_key
Delete the local key files afterwards. The rulesets allow deploy keys as a class, so rotation needs no ruleset change.
The workflow pushes main and the tag with the deploy key rather than
GITHUB_TOKEN because the rulesets can grant a deploy key a bypass. It calls
the build workflows directly, rather than letting the tag push trigger them, so
one run owns the whole release and the GitHub release is published only after
every build succeeds.