Releasing
July 1, 2026 · View on GitHub
keep-pressing versions itself from Conventional Commits — there is no manual version bump. This page covers how a release happens and how to activate the automation. Design rationale: ADR-0001.
keep-pressing is a single C# solution, so the setup is deliberately simple: one csproj holds the version, and release-please bumps it. (There is no separate engine, lockfile, or build channel to keep in sync.)
How a release happens (once activated)
- Conventional Commits land on
main(squash-merged PRs; the PR title is the commit). release-pleasekeeps a Release PR open that bumpsKeepPressing/KeepPressing.csproj's<Version>(thex-release-please-versionline) and updatesCHANGELOG.md. The version is derived from the commits:feat:→ minor,fix:/perf:→ patch,!/BREAKING CHANGE:→ major.- Add the
release: approvedlabel to the Release PR. Until it's there therelease-gatecheck fails the PR (so a release is never an accidental merge). - Merge the Release PR. release-please creates the GitHub Release as a draft
(config
"draft": true) and materializes thevX.Y.Zgit tag at the release commit ("force-tag-creation": true), then dispatchesrelease.yml. The tag is forced because a draft release otherwise has no git tag, so in the same run release-please can't see the just-cut release and would open a spurious "next" Release PR re-listing already-shipped commits. release.ymlruns: build → sign (approve in thereleaseenvironment) → publish (approve again). The publish step attaches the signedkeep-pressing-<version>-win-x64.zip- its
.sha256+ the CycloneDX SBOM to the draft (which already carries the tag from step 4) and publishes it. Assets land before publish, the order immutable releases require (a published immutable release can't gain assets afterward).
- its
You never hand-pick or hand-edit a version. The Release PR diff is the preview.
Release safety (defence in depth)
Cutting a real, immutable release is deliberately gated by several independent steps (ADR-0001), so an ambiguous instruction can't ship one by accident:
- Label gate — the Release PR can't merge until you add
release: approved(release-gateinci.yml). Adding or removing the label re-evaluates the gate automatically (CI runs onlabeled/unlabeledevents), so the check flips within seconds of approval. Makerelease-gatea required status check in branch protection for the gate to be binding. - Label guard —
autorelease: pendingis release-please's tracking label; without it the merged PR is never tagged/released. It can't be locked in GitHub, sorelease-label-guard.ymlreinstates it if it's removed from a release-please PR. (The humanrelease: approvedlabel is deliberately not guarded — removing it to un-approve is fine.) Labels themselves are declared in.github/labels.json(synced bylabels-sync.yml). - Manual merge — the Release PR is never auto-merged.
no-automerge-on-release-pr.ymlturns auto-merge back off if it's ever armed on a release-please PR (normal PRs unaffected). - No tag-triggered cascade —
release.ymlis started only by an explicit dispatch fromrelease-please.yml, never by a tag push, so a stray or manualvX.Y.Ztag starts nothing. - Two environment approvals — both the
signandpublishjobs pause on thereleaseenvironment (reviewer = the maintainer); the irreversible publish has its own approval. - Agent contract — automated tooling (incl. the AI assistant) will not merge the Release PR,
push a
v*tag, approve thereleaseenvironment, or runrelease.ymlwithpublish=truewithout an explicit, version-named instruction.
Activation (one-time)
release-please ships dormant: with the App secrets unset, release-please.yml runs
green and no-ops. It runs as a GitHub App because a tag pushed by the default
GITHUB_TOKEN does not trigger release.yml (GitHub's workflow-recursion guard) — so
the tag must be pushed by a different identity. The workflow mints a short-lived
installation token at runtime via actions/create-github-app-token.
- Create a GitHub App (org or personal). Repository permissions: Contents: Read & write and Pull requests: Read & write. No webhook needed.
- Install the App on the
keep-pressingrepo. - Generate a private key (
.pem) for the App and note its Client ID (shown at the top of the App's General settings page, e.g.Iv…). - Create an environment for the credential:
Settings → Environments → New environment → name it
release-please.- Deployment branches and tags → Selected branches → add
mainonly. - Do NOT add required reviewers (release-please must run unattended).
- Deployment branches and tags → Selected branches → add
- In that environment's Environment secrets, add:
RELEASE_PLEASE_CLIENT_ID= the App's Client IDRELEASE_PLEASE_PRIVATE_KEY= the full.pemcontents (paste the whole file,-----BEGIN…through…END-----; multi-line is fine)
- Ensure the
releaseenvironment already holds the SSL.com eSigner signing secrets (ES_USERNAME/ES_PASSWORD/CREDENTIAL_ID/ES_TOTP_SECRET) — thesignandpublishjobs use it as their approval gate (reviewer = the maintainer).
Why an environment, not a repository secret
A repository secret is readable by a workflow run on any branch. The App private key
carries contents: write + pull-requests: write, so we scope it: the release-please job
declares environment: release-please, and the environment's branch policy (main only)
means only the main-branch release-please run can read the key. This mirrors the signing
secrets in the release environment, with one deliberate difference: release-please's
environment has no required reviewers (the human gate is merging the Release PR).
A fine-grained PAT with the same two permissions also works — drop the
create-github-app-tokenstep and pass the PAT directly astoken:. The App is preferred (no human-tied credential; the token is short-lived and repo-scoped).
First release: the release-as pin
release-please-config.json sets "release-as": "0.1.0" to pin the first release to
0.1.0 — without it, release-please treats a first release from a 0.0.0 manifest as the
initial 1.0.0, wrong for a pre-1.0 project. After the first release is cut, remove
"release-as" in a follow-up PR (leaving it pins every future release to 0.1.0). From then
the manifest tracks the real version: a feat: proposes the next minor, a fix: the next patch.
Verify the first Release PR
On the first Release PR, confirm the diff bumps both:
KeepPressing/KeepPressing.csproj<Version>→0.1.0(thex-release-please-versionline)CHANGELOG.md
If <Version> doesn't move, check that the marker comment <!-- x-release-please-version -->
is still on the same line as <Version>…</Version> (the generic updater keys on it).