Skip to main content

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:

StageExampleHow it is produced
RC (pre-release)v0.11.0-rc.1rc command. Takes the next number while a series is open; otherwise opens a new series using bump
Stablev0.11.0stable command, promotes the newest RC
Patchv0.11.1patch 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 matches vX.Y.Z for stable tags and vX.Y.Z-rc.<N> for RCs, exactly. A tag like v1.0.0-rc.1 is 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 /v2 suffix from major 2 on. The workflow warns when it computes a major ≥ 2 while go.mod still lacks the suffix, but it does not block. Update go.mod and every internal import before shipping a v2, or go install [email protected] will fail.

Cutting a release

Run the Release workflow (Actions → Release → Run workflow) and set:

InputRequiredMeaning
commandyesrc, stable, or patch
bumpnominor (default), major, or patch. Only used by rc when it opens a new series
new_seriesnoFor rc only. Abandon an open RC series and start a new one from the latest stable
targetnoA commit sha/ref to tag. Defaults to main HEAD
dry_runnoCompute 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

  1. While a series is open — an RC whose base version is not yet a stable tag — rc simply takes the next number: v0.11.0-rc.4v0.11.0-rc.5. The bump input is ignored here, and the run logs a notice saying so.

  2. When no series is open — the newest RC was already promoted (it counts as consumed), or there are no RC tags at all — rc opens a new series from the newest stable, and bump decides 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.

  1. Creates the tag and opens a GitHub Release marked pre-release.
  2. 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.
  3. publish.yml pushes ghcr.io/abahmed/kwatch:v<X>.<Y>.<Z>-rc.<N> only — no latest, and the in-app upgrader does not nag RC users.
  4. 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.yml updates the preview version shown on kwatch.dev and deploys the site automatically.
  5. Adds a second commit — not pushed to main — that pins deploy/deploy.yaml to the RC image, and puts the tag on it. So kubectl apply against the RC tag installs the candidate, while deploy/deploy.yaml on main still 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.yaml has to say two different things at once: on main it 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 that kubectl apply against the tag actually installs the candidate. One commit cannot do both, so rc makes two:

CommitContainsPushed to mainTagged
1Release catalogs and metadata, when changed
2deploy/deploy.yaml image → 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 the stable guard compares by reachability instead of by sha.

stable and patch make a single commit — pushed and tagged — because they have no such conflict: the released manifest and main's manifest are the same thing.

stable — promote the latest RC

  1. Verifies main carries nothing the newest RC does not already have (git rev-list --count <rc>..origin/main must 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 of main on purpose (see the note under rc), so comparing shas would always fail.
  2. Bumps the stable manifest and chart versions, strips 🚧 Unreleased banners from README.md and every docs/*.md, commits them to main, and pushes (RELEASE_TOKEN required; see below): deploy/chart/Chart.yaml (version, appVersion), deploy/chart/README.md, and deploy/deploy.yaml (image tag).
  3. Creates the v<X>.<Y>.<Z> tag on that bump commit and opens a normal GitHub Release (gets latest). 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.
  4. publish.yml updates the stable version shown on kwatch.dev, clears the preview version, builds the site, and triggers one Render deployment.

Pinned-version invariant: on main, the chart version, deploy.yaml image tag, and chart README always point at the latest stable release. The README install command deliberately resolves the current stable release through kwatch.sh, while the interactive manager offers a published RC. Every manifest pin is bumped by the release workflow and never by feature PRs. The docs/ reference pages carry no version pins; a reference page that needs an install command links to the manager.

patch — hotfix tagged on main

  1. Merge the fix to main.
  2. Run the workflow with command: patch. It computes v<X>.<Y>.<Z+1> from the highest stable tag. The bump input does not apply — patch always moves the patch number.
  3. Bumps the pinned references to the patch version and pushes to main (same commit/step as stable, banners are not stripped — the next minor's features are still pending).
  4. 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, patch prints a warning: its bump commit moves main past the RC tag, so stable will refuse until you cut a fresh RC. Cut the RC again after the patch, then promote.

Hotfix isolation: tagging main HEAD also bundles any unreleased minor work already merged. If the hotfix must ship exactly on top of the previous stable, set target to 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 as target — no persistent release branches are ever kept. When target is set, the version bump is made on that commit and carried by the tag, but it is not pushed to main — pushing it would rewrite main's pinned versions backwards. Update main by hand if it should carry the new version.

The version-bump commit is pushed to the protected main branch, so all three commands require a RELEASE_TOKEN secret (a maintainer classic PAT with repo scope, allowed to bypass branch protection). rc needs 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 manual git push command to run.

RELEASE_TOKEN is also what makes the image get built. A GitHub Release created with the default GITHUB_TOKEN does not trigger other workflows, so publish.yml would 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.
  • check workflow is green on main (lint, build, unit tests with -race, integration tests).
  • helm lint + test_helm.sh pass for the released chart.
  • Release notes reviewed (generated automatically from merged commit titles).
  • README and docs/ contain no 🚧 Unreleased banners (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 main with the banner, so docs don't drift.
  • When a whole milestone rewrite is unreleased (e.g. the current v0.11.0-rc build), one top-of-file banner marks the entire README as documenting the dev build. Same 🚧 Unreleased marker, stripped the same way.
  • The stable command strips every banner line from README.md and docs/*.md and 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.md and 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 protected main branch, create tags/releases, trigger publish.yml, and Contents: Read and write access to abahmed/kwatch.dev. It must not be replaced by the default GITHUB_TOKEN.
  • RENDER_DEPLOY_HOOK_URL: the secret Deploy Hook URL copied from the kwatch.dev Render service settings. The workflow sends one POST after 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.