Release checklist
How to cut a kudurru release (written for v0.1.0-rc.1, then v0.1.0; no tag
exists yet). The pipeline is the branch workflows below plus
.goreleaser.yaml and .github/workflows/publish.yml (docs/architecture.md
§12 and D139, docs/deploy/README.md); this file is the order of the manual
steps around it and the checks that need the project owner's hardware.
Version names: VERSION in the repository root holds MAJOR.MINOR.PATCH
(no v, no suffix) and is the only version maintained by hand. The git
tags are v<version>-rc.<N> for release candidates and v<version> for the
release; goreleaser, the images, the Helm charts and kudurru version use
the version without the v. A release candidate publishes a GitHub
pre-release and does not move the latest image tags (prerelease: auto).
Release pipeline
feature ──PR──▶ main ──PR──▶ release-candidate ──PR──▶ release
dev-<PR> v<VERSION>-rc.<N> v<VERSION>
image pre-release, image release, image + latest
| Step | Workflow | What it publishes |
|---|---|---|
Push to a pull request against main (same repository, not Dependabot) | dev-image.yml | ghcr.io/robertherbaugh/kudurru:dev-<PR number> from the PR's head commit with the dashboard bundle, linux/amd64 and linux/arm64, unsigned. Every push overwrites the same tag; kudurru version reports <VERSION>-dev.<PR> and the commit. |
Push to release-candidate (a merge from main) | release-candidate.yml | Tags v<VERSION>-rc.<N> (N is one more than the highest candidate of that VERSION), then publish.yml: the GitHub pre-release, images <VERSION>-rc.<N>, <VERSION>-rc.<N>-debug and <VERSION>-rc.<N>-hostnetwork, charts and supervisor images at that version. |
Push to release (a merge from release-candidate) | release.yml | Tags v<VERSION>, then publish.yml: the GitHub release, images <VERSION>, latest, <VERSION>-debug, latest-debug, <VERSION>-hostnetwork and latest-hostnetwork, charts and supervisor images, The docs site is not part of this workflow: Cloudflare Workers Builds deploys it from the release branch (see below). |
Pull request into release-candidate or release | promotion.yml | Nothing: fails unless the source branch is main (for release-candidate) or release-candidate (for release), and dry-runs the tag the merge would create. |
hack/release-tag.sh rc|release computes the tags and refuses:
- a VERSION that is not
MAJOR.MINOR.PATCH; - a candidate or release for a VERSION whose
v<VERSION>tag already exists: bumpVERSIONonmainafter every release; - a release with no
v<VERSION>-rc.<N>, or whose tree differs from the latest candidate's: a release ships exactly what its last candidate shipped. Ifrelease-candidatewas pushed again while the promotion PR was open, wait for the new candidate to be tagged, then re-run thepromotioncheck (or thereleaseworkflow).
Re-running a workflow at the same commit reuses its tag. To re-publish an
existing tag by hand, run publish with workflow_dispatch and the tag, on
the release branch (a final tag) or release-candidate (a candidate);
publish refuses other branches, because the branch is part of the signing
identity that cosign verify checks (section 5).
The workflows push the tags with GITHUB_TOKEN; such tags start no other
workflow, which is why the branch workflows call publish.yml themselves.
Pushing a v* tag by hand publishes nothing. The docs site
(docs.kudurru.io) is deployed by Cloudflare Workers Builds when release
moves; no GitHub workflow publishes it.
Repository settings the pipeline relies on (once):
- Branches
release-candidateandreleaseexist (created frommainbefore the first promotion; creating a branch publishes nothing) and are protected: changes through pull requests only,promotionand thecichecks required. - Promotion pull requests are merged with a merge commit, not squash or
rebase (a branch ruleset can restrict the merge method per branch):
release-candidatemust keepmain's history, or the next promotion conflicts and the release tree check fails. - Promotion pull requests need an approving review by a maintainer
(CODEOWNERS for
.github/andhack/release-tag.sh): thepromotioncheck runs the workflow and script of the pull request itself, so a pull request that edits them can pass its own check. - Tag rulesets, if any, let GitHub Actions create
v*tags. - A branch ruleset refuses branches named
v*. publish.yml checks outrefs/tags/<tag>, so a branch named like the next tag can no longer stand in for it, but it would still confuse anyone checking out the bare name. - The Workers Builds project
kudurru-docsexists and builds thereleasebranch (site/README.md, "Publishing");docs.kudurru.ioanswers with the released docs, its images load, and thewwwlinks into it resolve. - The
ghcr.io/robertherbaugh/kudurrupackage grants this repository write access (it does when the first image was pushed by a workflow of this repository).
1. Prepare the release commit
-
mainis green:make test(race, envtest),make lint,make check-generated,make helm-check(kubeconform),make web-lint web-test web-buildandgo test ./web/...against the real bundle,make size. Do not commit the builtweb/dist/index.html. - Every row in docs/followups.md targeted at this release is closed or explicitly moved to the next one.
- Version strings agree:
VERSIONholds the version being released (the Makefile'sRELEASE_VERSION, what a build without a reachable tag reports, reads it);versionandappVersionincharts/kudurru/Chart.yamlandcharts/kudurru-crds/Chart.yaml(the release workflow re-stamps both from the tag when packaging, the committed values are whathelm install ./charts/kudurrufrom a checkout uses);- after changing the charts:
make deploy-assets(the kustomize bases carryapp.kubernetes.io/version) andmake rbac-manifestsifinternal/rbac/permissions.yamlchanged; - the image tags in
deploy/compose/**/docker-compose.yml(kudurru:<version>,kudurru-dnsdist:<dnsdist>-k<version>,kudurru-keepalived:<keepalived>-k<version>); for a release candidate keep the GA tags and say so in the release notes.
- CHANGELOG.md: the section for the version, with the date on the GA release.
- The distroless bases are pinned by digest (
tag@sha256:<index digest>) in.goreleaser.yaml(thebase_imagefallbacks), images/base/Dockerfile, the Dockerfile and images/hostnetwork/Dockerfile, the same digests everywhere (TestDockerfile). To bump:curl -sI -H 'Accept: application/vnd.oci.image.index.v1+json' https://gcr.io/v2/distroless/static/manifests/nonroot(anddebug-nonroot), takedocker-content-digest.
2. Dry-run the pipeline locally
goreleaser v2 (a downloaded release binary, checksum verified, or
go run github.com/goreleaser/goreleaser/v2@latest):
-
goreleaser checkvalidates.goreleaser.yaml. -
goreleaser build --snapshot --single-target --clean, then./dist/kudurru_linux_amd64_v1/kudurru version. Without a tag the snapshot is0.0.0-SNAPSHOT-<commit>; the before hook also runsmake release-manifests(kustomize intobin/release/). - With Docker available:
goreleaser release --snapshot --clean --skip=publish,sign(archives, deb/rpm/apk, SBOMs, ko images into the local daemon). - The before hooks run
make web-build, so every goreleaser run needs Bun onPATH;git checkout web/dist/index.htmlafterwards restores the committed placeholder. -
rm -rf distafterwards.
Dry runs for v0.1.0-rc.1 (not tagged yet):
- M5 sandbox, goreleaser 2.18.3:
checkand the single-target snapshot build succeeded (no Docker, no cosign identity). - v0.1 fix round (2026-10-11, at a57eb73 plus the digest pin of 82f9b8e, Docker 25, goreleaser 2.18.3
and syft 1.54.1, both checksum verified):
goreleaser checkvalidated the configuration;goreleaser release --snapshot --clean --skip=publish,signsucceeded in 39 s with a warm cache (5 min cold): 5 archives (linux and darwin amd64/arm64, compose), deb/rpm/apk for amd64 and arm64, SBOMs, checksums, and both ko images loaded into the daemon on the pinned distroless bases (kudurru versionin each printed0.0.0-SNAPSHOT-<commit>). syft must be onPATHfor the SBOM step.dist/removed afterwards. Signing is not exercised locally (keyless cosign needs the GitHub OIDC token).
3. Checks before GA (CI and the owner's hardware)
These gate the GA release (an rc may ship without them, listed as known gaps in the notes).
Two former items here run on GitHub's ubuntu-latest runners now:
- edge-check (
edgeworkflow, nocontinue-on-errorsince the v0.1 fix round):make edge-checkbuilds the kudurru image and both supervisor images (keepalived from source) and runsdnsdist --check-configon the 12 golden dnsdist files (the dynblocks-ebpf case as root with--cap-add SYS_ADMIN) andkeepalived -ton the 4 golden keepalived files. It passed locally from a clean clone with no build cache at a57eb73. Make theedgeworkflow a required check onmainonce it is green there. - kind (spike f,
kindworkflow,make kind-e2e): kind cluster, MetalLB L2,helm install kudurru-crdsandkudurruwithvip.provider=metallbandservice.dns.splitProtocolsboth ways;digan authoritative and a forwarded name through the LoadBalancer over UDP and TCP;kubectl get dnszones; a rolling restart with no unanswered query. Not yet run anywhere (the fix-round sandbox cannot start kind); make it required after green runs. Still manual: the edge Deployment with EndpointSlice membership.
On the owner's hardware:
- Two VMs with keepalived 2.4.3 (spike h), compose HA files or the
chart's keepalived DaemonSet: (1)
kudurru lameduckon the MASTER keeps the VIP and drains the node from dnsdist (D51); (2)kudurru vip drain dnsmoves the VIP within thefall/down timer; (3) powering off the MASTER moves the VIP; record failover times.kudurru doctorreportsvrrp: OK(no--legacy). - dnsdist spikes (a) console directives under YAML and (b) PROXYv2 +
chashed against a real dnsdist, with
kdig +quic/+tls/+httpsthrough the edge.
4. Promote and publish
- Release candidate: open a pull request from
mainintorelease-candidate, wait forpromotionand CI, merge it with a merge commit.release-candidate.ymltagsv<VERSION>-rc.<N>. - Release: once the candidate passed section 3, open a pull request from
release-candidateintoreleaseand merge it with a merge commit.release.ymltagsv<VERSION>. -
publish.ymlruns for the tag:make test,make helm-check, thengoreleaser release --clean(its before hook builds the dashboard bundle into web/dist), which publishes the GitHub release with archives, deb/rpm/apk,checksums.txt(cosign keyless signature and certificate), SBOMs,crds.yamlandinstall.yaml, and pushes the ko images, built onghcr.io/robertherbaugh/kudurru-base:<version>and<version>-debug(images/base, pushed and signed first and pinned by digest: the writable/var/lib/kudurruand/run/kudurru, D154;hack/volume-check.shruns on the result),ghcr.io/robertherbaugh/kudurru:<version>and<version>-debug(linux/amd64, linux/arm64), pluslatestandlatest-debugfor a release. Build provenance is attested for the archives, packages and checksums. - publish.yml's
testjob (read-only token, no Go cache) runsmake testandmake helm-checkon the tag before thereleasejob starts. The tools are pinned: goreleaser v2.18.3, helm v4.3.0, cosign v2.6.5, syft v1.54.1 (goreleaser's SBOMs; the runner image has none), and every action by commit SHA (Dependabot,.github/dependabot.yml;TestWorkflowActionsPinned). Every image gets SLSA build provenance pushed next to it (gh attestation verify oci://<image>@<digest> --owner robertherbaugh); the Dockerfile-built images (base, -hostnetwork, supervisors) also carry a BuildKit SBOM attestation, the ko images goreleaser's SPDX SBOM. - The workflow packages both charts with
--version <version> --app-version <version>, pushes them tooci://ghcr.io/robertherbaugh/chartsand cosign-signs each chart digest. - The
supervisor-imagesjob (afterrelease) builds and pushesghcr.io/robertherbaugh/kudurru-dnsdist:2.1.2-k<version>andghcr.io/robertherbaugh/kudurru-keepalived:2.4.3-k<version>fromimages/dnsdistandimages/keepalivedwithKUDURRU_IMAGE=ghcr.io/robertherbaugh/kudurru:<version>for linux/amd64 and linux/arm64 and cosign-signs each digest (the program versions come from the Dockerfile ARGs). Check both jobs succeeded; the image READMEs carry the GPL notices and source offer. - The
hostnetwork-imagejob (afterrelease) builds and pushesghcr.io/robertherbaugh/kudurru:<version>-hostnetwork(andlatest-hostnetworkfor a release) fromimages/hostnetworkwithKUDURRU_IMAGE=ghcr.io/robertherbaugh/kudurru:<version>: the ko image's binary withcap_net_bind_service=+ep, for the chart'shostNetwork.enabled. It cosign-signs the digest.
A release refuses a candidate whose publish did not finish: promotion.yml
(on the pull request into release) and release.yml (before tagging) run
hack/candidate-published.sh v<VERSION>-rc.<N>, which needs the
candidate's GitHub pre-release with checksums.txt. Re-run publish.yml for
the candidate tag first.
5. Verify what was published
- Checksums:
cosign verify-blob --certificate checksums.txt.pem --signature checksums.txt.sig --certificate-identity-regexp '^https://github\.com/robertherbaugh/kudurru/\.github/workflows/publish\.yml@refs/heads/release(-candidate)?$' --certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txt, thensha256sum -c checksums.txton the downloaded artifacts. The identity is publish.yml run from a release branch; a looser pattern (the whole repository) would also accept a signature made by any other workflow or branch of the repository. - Images:
cosign verify ghcr.io/robertherbaugh/kudurru:<version>with the same identity flags (publish.ymlalready ran this for both ko images:docker_signswithartifacts: manifestssigns the ko index digests), anddocker run --rm ghcr.io/robertherbaugh/kudurru:<version> versionprints<version>. - Charts:
helm pull oci://ghcr.io/robertherbaugh/charts/kudurru --version <version>andcosign verifyon the chart reference;helm installon the kind cluster from section 3. - CRDs:
kubectl apply --server-side -f crds.yamlfrom the release assets. - Packages: install the deb or rpm on a VM,
systemctl start kudurru,kudurru doctor.
6. After the release
- Announce with the CHANGELOG section and the known gaps.
- For a release candidate: collect issues, fix on
main, mergemainintorelease-candidateagain for the next-rc.<N>, or promote the candidate torelease. For the release: bumpVERSIONonmainto the next version (for example0.2.0) and the chart versions with it; the next promotion intorelease-candidatefails untilVERSIONis bumped.