Releasing
kube-workspaces is five repositories released together. The Helm chart’s
appVersion pins all four component images to a single number, so they share a
version: one platform version is materially easier to support than four drifting
ones.
Order matters
Release the four components first, then deploy. The chart’s appVersion
names images that must already exist, and the release workflow refuses to proceed
otherwise.
controller ─┐
api ─┤
proxy ─┼─> deploy (chart + appVersion)
frontend ─┘
Procedure
1. Component repositories
For each of controller, api, proxy, frontend:
gh workflow run release.yaml --repo kube-workspaces/<component> \
-f version=v0.3.0 -f dry_run=true
Review the generated notes in the run summary, then re-run with
dry_run=false. Tagging triggers the Docker workflow, which publishes
0.3.0, 0.3 and latest.
Wait for those builds to finish before moving on.
2. Bump the chart
In deploy, set both fields in helm/kube-workspaces/Chart.yaml:
version: 0.3.0 # the chart's own version — must equal the deploy tag
appVersion: "0.3.0" # the component version it pins — must already be released
The two are not required to match. version is the chart’s own release line
and must equal the tag; appVersion names the component release the chart pins.
A chart-only fix — an ingress path, a template guard — bumps version without
there being any new component build, so appVersion legitimately lags:
version: 0.2.2 # chart-only fix
appVersion: "0.2.1" # still pinning the 0.2.1 components
The release workflow enforces exactly that: version must equal the tag,
appVersion must not be ahead of it, and images must exist at appVersion.
Then verify and merge:
make test-lint
make test-upgrade # installs the published chart, upgrades to local, rolls back
Merging to main publishes the chart to GHCR. The publish workflow refuses to
overwrite an existing version.
3. Release deploy
gh workflow run release.yaml --repo kube-workspaces/deploy \
-f version=v0.3.0 -f dry_run=true
The workflow checks, before doing anything irreversible, that:
- the version is
vX.Y.Zand the tag does not already exist -
Chart.yamlversionandappVersionboth match - a matching image is published for all four components
Re-run with dry_run=false to tag and publish.
4. Update the website
Bump the version badge in kube-workspaces.github.io.
Release notes
Notes are generated, not hand-written. scripts/release-notes.sh emits the
whole body:
- A preamble that classifies the commits since the last tag and leads with what the reader needs to know.
- Upgrade instructions tailored to the repo.
-
A categorised “What’s Changed” list, grouped per the headings in
.github/release.yml— identical in all five repos, and enforced byscripts/check-release-config.sh.
The commit list is built from commit subjects rather than left to GitHub’s
--generate-notes, which groups commits by pull request and keys categories
off PR labels. Commits that land as direct pushes to main — common in this repo,
see the quirk below — have no PR and would otherwise produce an empty list.
Components with no functional changes
A component often has nothing but CI and docs changes in a cycle, and still gets tagged to hold the version line. The preamble says so explicitly:
No functional changes in this release.
The N commits since
vX.Y.Zare CI, tooling and documentation only. This version exists to keep the kube-workspaces components aligned […] Upgrading is safe and changes no behaviour.
This is deliberately conservative. A commit only counts as non-functional if it
carries a ci:, build:, docs:, chore:, style: or test: prefix.
Anything unrecognised counts as functional, so an unprefixed commit prevents
the claim and is reported as unclassified — better to under-claim than to tell
someone a release is inert when it is not.
Breaking changes (feat!:, or any prefix with !) produce a warning callout at
the top instead.
Labelling
Categorisation keys off PR labels. Available in every repo:
| Label | Section |
|---|---|
breaking |
⚠️ Breaking changes |
security |
🔒 Security |
enhancement |
✨ Features |
bug |
🐛 Fixes |
documentation |
📚 Documentation |
ci, chore
|
🔧 CI and tooling |
dependencies |
⬆️ Dependencies |
Unlabelled PRs land in Other changes rather than vanishing. duplicate,
invalid, wontfix and question are excluded.
Versioning
Semantic versioning, judged against the platform, not individual components:
- Patch — fixes only, no new capability
- Minor — new capability, or a deprecation with a working fallback
- Major — anything requiring user action to upgrade
A field deprecated with a documented, tested fallback is a minor. Removing it later is a major.
Preview notes locally
scripts/release-notes.sh --repo deploy --from v0.2.0 --to HEAD
Known quirks
The v0.2.0 publish run shows red
The Publish Helm Chart run triggered by the v0.2.0 release event failed.
The chart is published and the release is complete — this is cosmetic.
The version guard was too blunt at the time: on a release event HEAD is the
tagged commit, which of course changed the chart, since bumping the version is how
the release was prepared. The push to main had already published it, so there
was nothing left to do. Fixed in helm-publish.yaml, which now only enforces the
“changed without a bump” rule on pushes to main.
Re-running does not clear it: a re-run checks out the tag, which predates the fix.
Clearing it would mean re-tagging a published release, which is not worth doing.
Releases from v0.2.1 onward are unaffected.
main history includes direct pushes
Branch protection on deploy requires pull requests but has
enforce_admins: false, so commits made with an admin token can and did land
directly on main — most of the test-suite and CI work arrived that way while it
was being iterated against real runners.
This used to leave the generated release notes with an empty “What’s Changed”
list: GitHub groups commits by PR, and a direct push has no PR. The notes are now
generated from commit subjects by scripts/release-notes.sh, so they populate
regardless of how the commits landed.
The four component repositories were changed via PR throughout. If direct pushes
to deploy are unwanted, set enforce_admins: true:
gh api -X PATCH repos/kube-workspaces/deploy/branches/main/protection/enforce_admins
Note this also blocks --admin merges, so every PR would then need a reviewer.