Releasing
kube-workspaces is five repositories released together. The four components
share one component version (e.g. v0.6.0), which the Helm chart’s
appVersion pins: one component version is materially easier to support than
four drifting ones. The chart itself has its own version line (e.g.
0.6.22) that names the deploy tag and the website badge — the chart is
released more often than the components (chart-only fixes), so the deploy
version routinely outruns the component version. The desktop client is an
additional independent line, released alongside them only when it has changes
(see “Order matters”). The workspace agent is a second independent line with
the same standalone shape — own semver, tag-driven archives plus the unsigned
Windows MSI, released only when it has changes (see §1B).
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.
The desktop client is released early (before the components), but only when it
has new commits since its last release. It is standalone: it is not a Docker
image, is not pinned by the chart, and keeps its own vX.Y.Z semver lineage
— always bumped on top of its own history, never matched to the component or
deploy version. It has no ordering dependency on the rest, and goes early so the
newest client is on the downloads page by the time a platform release lands —
the client consumes platform features via the proxy, and we do not want a window
where the published client cannot talk to a just-released platform.
controller ─┐
api ─┤
proxy ─┼─> deploy (chart + appVersion)
frontend ─┘
desktop-client ──> standalone binaries (not in the chart); released only when it has changes
workspace-agent ──> standalone guest binaries (archives + MSI, not in the chart); released only when it has changes
Procedure
The steps below can be carried out interactively with opencode via the
create-new-release skill in this repo. It walks each stage with the ordering,
dry-run and artifact gates enforced; treat this document as the source of
truth and keep the two in step.
0. Pre-flight: CI must be green
Before heading into the component releases, check that nothing on the org’s default branches is failing — a release cut over red CI bakes the failure in, and a re-run cannot be attached to the tag:
make show-ci-status
The script inspects the most recent workflow runs on each repo’s default branch
and flags any repo whose newest commit has a failing run (failed / yes).
Failures already fixed on the tip are reported in the NOTES column as older
refs rather than action items, so the table answers the release question — does
anything need attention now — and not “were there ever red builds”.
# Restrict to specific repos, or widen the window:
scripts/org-project-recent-actions-status.sh --repo controller --repo api
scripts/org-project-recent-actions-status.sh --runs 25
Address the failing repos before proceeding. The script exits non-zero when any
repo needs action. This matters for the desktop client in particular (step 1):
its release is tag-driven, so a red Build run at the tag’s commit neither
blocks the tag nor gets re-run — it ships broken binaries.
1. Desktop client
desktop-client does not use the component release.yaml workflow. Decide
first whether a release is warranted — it is released only when there are new
commits on main since its last release tag:
# commits on main since the last release; skip this step entirely when it is 0
gh api repos/kube-workspaces/desktop-client/compare/<last-tag>...main \
--jq '.ahead_by'
If ahead_by is 0, do not tag and do not create a release — the existing
download on the releases page stays current and the rest of the procedure
continues unaffected.
If there are changes, tag it on its own version line — vX.Y.Z bumped over
its own history (e.g. v0.2.0 → v0.2.1), never copied from the platform
version:
# in kube-workspaces/desktop-client
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
Pushing the tag triggers build.yml, which cross-builds the per-platform
archives, composes the release notes (download links, install steps, an unsigned
warning) and publishes the GitHub Release — binaries plus SHA256SUMS. There is
no dry-run preview; the create-new-release skill shows a preview of the
commits that will ship before you tag, but the composed notes only exist after
publish — review them on the releases page and fix anything wrong in a follow-up
patch release on the same line.
The client must be green first. make show-ci-status flags it failed if the
newest commit’s runs (notably Build) are not passing.
1B. Workspace agent
workspace-agent follows the same standalone shape as the desktop client:
it does not use the component release.yaml workflow. Release it only
when there are new commits on main since its last release tag, on its
own vX.Y.Z semver lineage — never matched to the client, component or
deploy versions:
# commits on main since the last release; skip this step entirely when it is 0
gh api repos/kube-workspaces/workspace-agent/compare/<last-tag>...main \
--jq '.ahead_by'
# in kube-workspaces/workspace-agent
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
Pushing the tag triggers build.yml, which builds the per-target archives
and the unsigned Windows MSI and publishes the GitHub Release — binaries
plus SHA256SUMS. Guests pick it up through the normal install path
(install.ps1, shipped in the archives and the MSI payload, also registers
the interactive logon serve task), so cut it whenever guest-side fixes are
validated — inside a coordinated platform release or on its own. Like the
client there is no dry-run preview: show the commits that will ship before
tagging, review the composed notes and assets on the releases page after
publish, and fix anything wrong in a follow-up patch release on the same
line. The agent must be green first — a red Build at the tag’s commit
ships broken binaries the same way.
2. 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.
3. Bump the chart
In deploy, advance both fields in helm/kube-workspaces/Chart.yaml on their
own lines:
version: 0.6.22 # the chart's own version — must equal the deploy tag (bare)
appVersion: "0.6.0" # the component version it pins — must already be released
The two are not required to match, and usually will not. 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.
There is no “full release sets version == appVersion” step. A full component
release releases components, then bumps appVersion to that new component
version while version advances on the chart’s own independent line — the two
numbers do not need to converge.
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.
4. 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.yamlversionmatches the release tag (andappVersionnames an existing component release) - a matching image is published for all four components
Re-run with dry_run=false to tag and publish.
5. Update the website
Bump the version badge in
kube-workspaces.github.io
to the deploy version (the release tag just cut in step 4 — e.g. v0.6.22)
and, if the desktop client moved (step 1), point its download link at the new
release.
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 across every kube-workspaces repo that publishes releases, 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’s component version, 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.
The chart/deploy version line is not held to the same semver surface — it
advances patch-by-patch (e.g. 0.6.21 → 0.6.22) for each deploy release,
whether or not a component release rode along. Semantic versioning above applies
to the component version.
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.