Releasing kwatch
This document describes how kwatch is branched and released. Releases are cut with the
.github/workflows/release.yml workflow using workflow_dispatch. It creates the version
tag and a GitHub Release; .github/workflows/publish.yml then builds and pushes the
multi-arch container image to ghcr.io/abahmed/kwatch, publishes the stable Helm
chart, and syncs release metadata to kwatch.dev.
Branch model
One branch: main. All changes land here via short-lived PR branches
(feat/*, fix/*, refactor/*). main is protected (review + green check required).
There are no release or develop branches. Every release — RC, stable, or patch — is a
tag on main history plus a GitHub Release.
Versioning
Semantic versioning, v-prefixed:
| Stage | Example | How it is produced |
|---|---|---|
| RC (pre-release) | v0.11.0-rc.1 | rc command. Takes the next number while a series is open; otherwise opens a new series using bump |
| Stable | v0.11.0 | stable command, promotes the newest RC |
| Patch | v0.11.1 | patch command, increments the highest stable tag |
Versions are always computed from existing tags — maintainers never type a version, they only choose which part moves.
Tag hygiene: only
-rc.<N>pre-releases are recognised. The workflow matchesvX.Y.Zfor stable tags andvX.Y.Z-rc.<N>for RCs, exactly. A tag likev1.0.0-rc.1is ignored by both, so do not create other pre-release forms — the version computation will behave as if it does not exist.
Going to v2: Go requires the module path to carry a
/v2suffix from major 2 on. The workflow warns when it computes a major ≥ 2 whilego.modstill lacks the suffix, but it does not block. Updatego.modand every internal import before shipping a v2, orgo install [email protected]will fail.
Cutting a release
Run the Release workflow (Actions → Release → Run workflow) and set:
| Input | Required | Meaning |
|---|---|---|
command | yes | rc, stable, or patch |
bump | no | minor (default), major, or patch. Only used by rc when it opens a new series |
new_series | no | For rc only. Abandon an open RC series and start a new one from the latest stable |
target | no | A commit sha/ref to tag. Defaults to main HEAD |
dry_run | no | Compute the version and notes, then stop. Nothing is tagged or built |
Every run writes a summary on the run page: the version, whether it is a pre-release, the tagged commit, the image tags Publish will push, the release URL, and the full notes. You never need to read the logs to find out what was cut.
rc — pre-release
-
While a series is open — an RC whose base version is not yet a stable tag —
rcsimply takes the next number:v0.11.0-rc.4→v0.11.0-rc.5. Thebumpinput is ignored here, and the run logs a notice saying so. -
When no series is open — the newest RC was already promoted (it counts as consumed), or there are no RC tags at all —
rcopens a new series from the newest stable, andbumpdecides which part moves:bumpFrom v0.11.0Use for minor(default)v0.12.0-rc.1new features patchv0.11.1-rc.1a fix you want to soak before shipping majorv1.0.0-rc.1breaking changes
If an old RC series should be abandoned before promotion, set new_series: true.
Then choose the desired bump; bump: major starts v1.0.0-rc.1 from the latest
stable even when an older v0.11.0-rc.* series is still open. Use this only when
the older series is intentionally no longer the release target.
- Creates the tag and opens a GitHub Release marked pre-release.
- Release notes compare against the previous RC while a series is open, so each RC lists only what is new since the last one. The first RC of a series compares against the newest stable.
publish.ymlpushesghcr.io/abahmed/kwatch:v<X>.<Y>.<Z>-rc.<N>only — nolatest, and the in-app upgrader does not nag RC users.- Regenerates the configuration, feature, and provider catalogs and pushes any
resulting release metadata commit to
main. The README install instructions are version-free. After publishing,publish.ymlupdates the preview version shown onkwatch.devand deploys the site automatically. - Adds a second commit — not pushed to
main— that pinsdeploy/deploy.yamlto the RC image, and puts the tag on it. Sokubectl applyagainst the RC tag installs the candidate, whiledeploy/deploy.yamlonmainstill points at the latest stable. See the note below.
Run rc as often as needed until the candidate stabilizes.
Why an rc makes two commits.
deploy/deploy.yamlhas to say two different things at once: onmainit must pin the latest stable image, so that copying it out of the repo browser can never install a preview build; at the RC tag it must pin the RC image, so thatkubectl applyagainst the tag actually installs the candidate. One commit cannot do both, sorcmakes two:
Commit Contains Pushed to mainTagged 1 Release catalogs and metadata, when changed ✅ — 2 deploy/deploy.yamlimage → the new RC❌ never ✅ The second commit exists only on the tag. That is why an RC tag sits exactly one commit ahead of
main, and why thestableguard compares by reachability instead of by sha.
stableandpatchmake a single commit — pushed and tagged — because they have no such conflict: the released manifest andmain's manifest are the same thing.
stable — promote the latest RC
- Verifies
maincarries nothing the newest RC does not already have (git rev-list --count <rc>..origin/mainmust be 0); otherwise it fails and you must cut a fresh RC first. This is a reachability check, not sha equality — an RC tag is one commit ahead ofmainon purpose (see the note underrc), so comparing shas would always fail. - Bumps the stable manifest and chart versions, strips
🚧 Unreleasedbanners fromREADME.mdand everydocs/*.md, commits them tomain, and pushes (RELEASE_TOKENrequired; see below):deploy/chart/Chart.yaml(version,appVersion),deploy/chart/README.md, anddeploy/deploy.yaml(image tag). - Creates the
v<X>.<Y>.<Z>tag on that bump commit and opens a normal GitHub Release (getslatest). Because the tag commit carries the bumped files, the raw/kwatch/vX.Y.Z/deploy/...refs and the chart at the tag match the released version. publish.ymlupdates the stable version shown onkwatch.dev, clears the preview version, builds the site, and triggers one Render deployment.
Pinned-version invariant: on
main, the chart version,deploy.yamlimage tag, and chart README always point at the latest stable release. The README install command deliberately resolves the current stable release throughkwatch.sh, while the interactive manager offers a published RC. Every manifest pin is bumped by the release workflow and never by feature PRs. Thedocs/reference pages carry no version pins; a reference page that needs an install command links to the manager.
patch — hotfix tagged on main
- Merge the fix to
main. - Run the workflow with
command: patch. It computesv<X>.<Y>.<Z+1>from the highest stable tag. Thebumpinput does not apply —patchalways moves the patch number. - Bumps the pinned references to the patch version and pushes to
main(same commit/step asstable, banners are not stripped — the next minor's features are still pending). - Creates the tag on that bump commit and opens a normal release.
A patch blocks a pending promotion. If an RC is still waiting to be promoted,
patchprints a warning: its bump commit movesmainpast the RC tag, sostablewill refuse until you cut a fresh RC. Cut the RC again after the patch, then promote.
Hotfix isolation: tagging
mainHEAD also bundles any unreleased minor work already merged. If the hotfix must ship exactly on top of the previous stable, settargetto the hotfix commit sha (a detached one-off tag). For a strictly isolated hotfix line you can also create a throwaway branch locally, cherry-pick, and pass its sha astarget— no persistent release branches are ever kept. Whentargetis set, the version bump is made on that commit and carried by the tag, but it is not pushed tomain— pushing it would rewritemain's pinned versions backwards. Updatemainby hand if it should carry the new version.
The version-bump commit is pushed to the protected
mainbranch, so all three commands require aRELEASE_TOKENsecret (a maintainer classic PAT withreposcope, allowed to bypass branch protection).rcneeds it too, since it may push regenerated catalogs. If the secret is missing or the push fails, the workflow stops before tagging and prints the manualgit pushcommand to run.
RELEASE_TOKENis also what makes the image get built. A GitHub Release created with the defaultGITHUB_TOKENdoes not trigger other workflows, sopublish.ymlwould never run and the release would ship with no container image.
Previewing a release
Set dry_run: true to work out the version and the release notes and then stop. Nothing is
tagged, no release is opened, no image is built. The job is titled Preview and the run
summary shows the version it would cut, the image tags it would push, and the full notes.
Use it whenever you are unsure which version a command will produce — for example before a
rc that opens a new series, where the answer depends on bump.
RC → stable gates
An RC should not be promoted until all of these hold:
- RC has been published for at least 2 weeks of soak (unless a critical fix is blocking).
- No open critical issues / known regressions against the RC.
-
checkworkflow is green onmain(lint, build, unit tests with-race, integration tests). -
helm lint+test_helm.shpass for the released chart. - Release notes reviewed (generated automatically from merged commit titles).
- README and
docs/contain no🚧 Unreleasedbanners (stripped automatically on stable).
README, docs, and unreleased features
Feature code merges to main immediately. A feature's README section also merges
immediately, but under a banner while unreleased:
> **🚧 Unreleased** — ships in `v0.12.0`. Not available in stable installs yet.
- Unreleased sections stay visible on
mainwith the banner, so docs don't drift. - When a whole milestone rewrite is unreleased (e.g. the current
v0.11.0-rcbuild), one top-of-file banner marks the entire README as documenting the dev build. Same🚧 Unreleasedmarker, stripped the same way. - The
stablecommand strips every banner line fromREADME.mdanddocs/*.mdand bumps the pinned manifest and chart references in the same commit. - Maintainers never touch version numbers by hand; the pinned-version invariant and the
banner convention are enforced by
CONTRIBUTING.mdand the gate checklist above.
Version-free install instructions
The repository README intentionally does not pin a stable or preview version. The
interactive kwatch.sh manager resolves the latest stable release by default and
offers a published RC during installation or upgrade. Release-specific image and
chart pins remain in the manifests and chart metadata maintained by the workflow.
Releasing the Helm chart
Publishing is automatic. The publish_helm_chart job in
.github/workflows/publish.yml runs from the stable GitHub Release tag, verifies that
deploy/chart/Chart.yaml has the same version, runs helm lint, packages the chart, and
updates static/charts/index.yaml in the abahmed/kwatch.dev repository.
The following update_website job updates src/data/releases.json, builds the Docusaurus
site, pushes the metadata commit, and sends one Render deploy hook. RC releases skip the
Helm chart job but still update the preview version on the site. Docusaurus serves the
generated files under static/charts at https://kwatch.dev/charts.
Two repository secrets are required in abahmed/kwatch:
RELEASE_TOKEN: a token with permission to push the protectedmainbranch, create tags/releases, triggerpublish.yml, and Contents: Read and write access toabahmed/kwatch.dev. It must not be replaced by the defaultGITHUB_TOKEN.RENDER_DEPLOY_HOOK_URL: the secret Deploy Hook URL copied from thekwatch.devRender service settings. The workflow sends onePOSTafter pushing the website metadata.
Disable Render's normal Auto-Deploy for the kwatch.dev service when using this hook;
otherwise the same website push can start two deploys.
If the token is missing or cannot push to kwatch.dev, the chart job fails without changing
the website repository. The container image job and GitHub Release are separate jobs, so the
release remains inspectable and the publish workflow can be safely re-run after fixing the
token. The final verification job fails the release workflow if the image, website sync, or
public chart (for stable releases) is not successful.
Upgrader notes
The container image bakes the full version string (the release tag name, v-prefixed). The
in-app upgrader only runs on stable and patch images: it compares the baked version
against the latest non-pre-release GitHub Release and notifies on a newer one, recording
the notified version in a ConfigMap so users are nudged once. RC builds skip the check
entirely (CheckUpdates returns early when the baked version contains -rc) — RC users
opted into the dev channel and are never nagged toward stable. Keep this in mind: the baked
version must equal the release tag name (v-prefixed), or the equality comparison in the
upgrader never matches.