This is the operational runbook for cutting a KubeFleet release and for recovering when a release run fails partway through. It covers how a release is produced; what the version numbers mean and how long each release is supported are covered in VERSIONING.md and SECURITY.md.
| Artifact | Location | Stable (v0.4.0) |
Release candidate (v0.4.0-rc.1) |
|---|---|---|---|
Agent images (hub-agent, member-agent, refresh-token) |
ghcr.io/kubefleet-dev/kubefleet/<image> |
:v0.4.0 and :0.4.0 |
:v0.4.0-rc.1 only |
CRD bundle (kubefleet-crds-<tag>.tgz + .sha256) |
GitHub Release asset | Yes | Yes |
| Helm charts (OCI) | oci://ghcr.io/kubefleet-dev/kubefleet/charts/<chart> |
Yes | No |
| Helm charts (index) | https://kubefleet-dev.github.io/kubefleet/charts |
Yes | No |
| GitHub Release | Releases page | Published | Published, flagged pre-release |
Release candidates deliberately get no short image alias and never enter the
public chart index: the short-tag namespace and the helm repo index are
reserved for releases users can safely pin to. Testers install an RC from its
full tag.
All images and charts are built from the tag itself, so everything that ships must be merged before the tag is pushed.
-
Confirm
main(or therelease-0.Ybranch) is green and carries every change intended for the release, including backports — see CONTRIBUTING.md. -
Tag and push. The tag must match
vMAJOR.MINOR.PATCHorvMAJOR.MINOR.PATCH-rc.N; any other shape is rejected before anything is published.git tag -a v0.4.0-rc.1 -m "v0.4.0-rc.1" git push upstream v0.4.0-rc.1 -
Watch the
Releaseworkflow. It publishes the GitHub Release only after every artifact has been produced. -
For a stable release, repeat with the final tag (for example
v0.4.0) once the RC has soaked.
A release can also be started from the Actions tab via Run workflow on the
Release workflow, passing the tag as an input. Two preconditions apply:
- The tag must already exist. The workflow passes
--verify-tag, so a dispatch naming a tag that was never pushed fails instead of inventing one at the head of the default branch. - The tag must be one cut after this workflow landed. A dispatch runs the
workflow definition from the selected branch but checks out the tag, and the
jobs call scripts under
hack/release/; against an older tag that predates them, the first job fails immediately with "No such file or directory". Nothing is published when it does. Tags on arelease-0.Ybranch that predates this workflow are unaffected — they carry their own contemporary workflow.
.github/workflows/release.yml is the single owner of a release. Its job graph:
setup validate the tag; derive registry, version, prerelease
└── create-draft-release create (or reuse) the GitHub Release as a draft
├── publish-images multi-arch buildx push, then verify both platforms
│ ├── publish-charts-oci stable only; helm push + appVersion check
│ └── publish-charts-pages stable only; rewrite the gh-pages index
└── publish-crds package the CRDs, upload the bundle to the draft
publish-release needs ALL of the jobs above; verifies the release's
assets, then flips the draft to published
Two properties matter when something goes wrong:
- The GitHub Release is a draft until the very end. No release page, release
notes, or release asset is visible until every producer has succeeded. Note
the scope: this covers the release, not the registry. Images and charts are
publicly pullable the moment their own job succeeds, so a run that fails after
publish-imageshas leftghcr.io/.../hub-agent:v0.4.0reachable even though no release mentions it. - Charts wait for images. A chart whose
appVersionpoints at images that do not exist yet is broken on arrival, so the chart jobs run only after the images are pushed and verified.
The two jobs with real branching logic — create-draft-release and
publish-release — live in hack/release/ rather than inline
in the workflow, and are covered by hack/release/test-release-scripts.sh,
which CI runs on every change to either.
The normal recovery is Re-run failed jobs on the workflow run. Jobs that
already succeeded are not re-run, and every job is safe to repeat: the CRD
upload uses --clobber, create-draft-release reuses the draft it created the
first time, and image pushes rewrite the same tags.
Re-running publish-images rebuilds from source rather than reproducing the
earlier build byte-for-byte, so the tag ends up pointing at a new digest. That
is harmless while the release is still a draft — nothing has been announced yet
— but it is why a re-run is not an option once a release has been published.
| Where it failed | What is already public | What to do |
|---|---|---|
setup |
Nothing | The tag is malformed. Delete it, fix, re-tag. |
create-draft-release |
Nothing | See Re-releasing an existing tag if it refused because the release is already published. |
publish-images |
Any images pushed before the failure (make push builds hub-agent, member-agent, then refresh-token in order) |
Fix, then re-run failed jobs. |
publish-crds |
Possibly the images — it runs in parallel with publish-images, not after it |
Fix, then re-run failed jobs. |
publish-charts-oci / publish-charts-pages |
Images; CRD bundle is attached to the still-hidden draft | Fix, then re-run failed jobs. The release stays a draft until the charts land. |
publish-release |
Images, charts | The asset check found the draft incomplete or its bundle failed its own checksum. Inspect gh release view <tag>, re-upload, re-run failed jobs. |
publish-charts-pages serializes across all releases, because the action it
uses rewrites the whole gh-pages branch. GitHub keeps at most one pending
entry per concurrency group, so if three stable releases overlap, the middle
one's pages job is cancelled rather than queued. That leaves its release as
a draft with everything else done; Re-run failed jobs finishes it. Cutting
stable releases one at a time avoids the situation entirely.
If the fix requires a code change, the tag must move or be replaced — see below. Do not rebuild a different commit under a tag that already pushed images.
create-draft-release refuses to run against a release that is already
published. This is deliberate: consumers may already have pinned the images and
charts that release advertises, and a second run would replace them in place
with a different build.
-
If the release should not have gone out (wrong commit, broken build): delete the GitHub Release and the tag, then cut the next tag rather than reusing the old one —
-rc.N+1for a release candidate, or the next patch version for a stable release. Container tags that have been pulled are not safely reusable, and the CRD bundle checksum users recorded would change under them. Because the bad images stay pullable under their original tag (see Abandoning a release), also delete those package versions if the build was actually broken rather than merely superseded. -
If only one artifact is missing (for example a chart publish that was fixed after the release went public): publish that artifact manually rather than re-running the whole workflow. Note that the OCI registry and the Pages index are published by two different jobs and need two different fixes:
# OCI charts make helm-push REGISTRY=ghcr.io/kubefleet-dev/kubefleet/charts \ TAG=v0.4.0 CHART_VERSION=0.4.0 # Pages index: re-run the publish-charts-pages job from the workflow run, # which is the only thing that rewrites the gh-pages branch.
If a release is called off, delete the draft and the tag so the next attempt starts clean:
gh release delete v0.4.0-rc.1 --yes
git push upstream :refs/tags/v0.4.0-rc.1
git tag -d v0.4.0-rc.1That removes the release and the tag, but not the artifacts the producer jobs already published. Those outlive the release and have to be cleaned up deliberately:
- Images.
ghcr.io/kubefleet-dev/kubefleet/<image>:v0.4.0— and the short alias:0.4.0for a stable tag — stay publicly pullable. The next tag is a different version, so it never supersedes them. Delete the package versions (gh api --method DELETE /orgs/kubefleet-dev/packages/container/<pkg>/versions/<id>) if the build was broken rather than merely renumbered. - Charts. If
publish-charts-ociran, the chart is in the OCI registry and needs the same treatment. Ifpublish-charts-pagesran, thegh-pagesindex already advertises the abandoned version, and the next stable release will not remove it — the entry has to be dropped fromcharts/index.yamlon thegh-pagesbranch by hand.
Abandoning a stable release after the chart jobs have run is therefore not cleanly reversible. Soak on release candidates, which publish neither chart.
- At the first RC of a new minor, cut the matching
release-0.Ybranch. From then on, fixes land onmainand are backported with thecherry-pick/0.Ylabels described in CONTRIBUTING.md. - Verify the published release page lists the CRD bundle and its checksum, and
that the generated notes look right — they come from the
release-note/*labels on the PRs in the release. - One-time, at the first stable release cut by this workflow: the
gh-pageschart index carries stalehub-agent 0.1.0andmember-agent 0.1.0entries from before the index was given the real release version. The publish step merges into the existing index rather than replacing it, so those entries survive andhelm search repo kubefleet --versionskeeps offering0.1.0. Delete them fromcharts/index.yamlon thegh-pagesbranch (and the matchingcharts/*-0.1.0.tgz) once a correctly-versioned entry exists.
- VERSIONING.md — versioning scheme, agent skew, upgrade order.
- SECURITY.md — supported versions and security-patch policy.
- CONTRIBUTING.md — PR conventions, release-note labels, and backport policy.