Use when preparing or executing a release - runs the authoritative local gates, verifies release content, and, when explicitly authorized, pushes the release tag
66
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Prepare a new release by generating changelog entries, updating version references, and creating release notes.
/prepare-releaseDo not create release tags just because this skill was invoked. By default, prepare only.
When Karim explicitly says to do the release (for example "release time" or "do the release"), run the full local release gates, create and push the annotated tag yourself, then confirm the lightweight publication workflow.
Default job:
Maintainer-authorized release job:
master is clean and up to date.github/workflows/publish-release.yaml publish the already-verified tagThe repository has tag-driven release automation in .github/workflows/publish-release.yaml.
Important details:
v* tags and has no manual-dispatch path.CHANGELOG.md, specifically everything under ## [Unreleased] until the next ## [ heading.[Unreleased] section.ncipollo/release-action.Therefore:
CHANGELOG.md is the release-content Markdown file.## [Unreleased] until after the release tag is pushed.[Unreleased] to [vX.Y.Z] - YYYY-MM-DD before tagging unless you are also bypassing the workflow and manually providing release notes.gh release create during the normal path; the tag workflow owns publication. Use it only after re-running the local integrity gates if publication fails.master pull-request path, then tag the resulting merged commit.CHANGELOG.md: reset ## [Unreleased] to an empty placeholder and move the released notes under ## [X.Y.Z] - YYYY-MM-DD. Merge that cleanup through a release-maintenance pull request.## [Unreleased]; otherwise the next tag workflow will publish stale notes again.CHANGELOG.md carries the release content.kube-hetzner-knowledge.jsondata) and confirm its meta.version matches
the release. The Custom GPT was retired 2026-07-13 — the agent skills
(installed via npx skills add kube-hetzner/terraform-hcloud-kube-hetzner)
are the assistant channel; the knowledge file feeds future tooling (MCP).GitHub-generated notes and the changelog expose the people whose work landed since the previous tag. Original PR submitters MUST remain visible — credit where credit is due.
review-pr skill): community contributions keep the contributor as commit author in master history. Squash only when merging a contributor-only PR directly; use a merge commit when we pushed fixes on top; cherry-pick with preserved authorship or Co-authored-by: trailers only when partially adopting or porting work.git log <prev-tag>..HEAD --format='%an <%ae>' | sort -u — every community contributor whose fix is in the release must be listed. If someone is missing, fix history/credit BEFORE tagging (after tagging it is public and immutable).mergedAt. For PRs integrated indirectly through a release branch, also verify the recorded headRefOid is an ancestor of the release target. A closed-but-unmerged accepted PR is a release-process defect; repair the integration before tagging instead of compensating with comments.digraph release_flow {
rankdir=TB;
node [shape=box];
analyze [label="1. Analyze changes since last release"];
classify [label="2. Classify release type"];
changelog [label="3. Update CHANGELOG.md"];
badges [label="4. Update version badges"];
gpt [label="5. Update GPT knowledge"];
notes [label="6. Verify CHANGELOG.md release content"];
commit [label="7. Commit preparation"];
release [label="8. If explicitly authorized: tag + monitor workflow"];
analyze -> classify;
classify -> changelog;
changelog -> badges;
badges -> gpt;
gpt -> notes;
notes -> commit;
commit -> release;
}REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
# Get latest release tag
LATEST=$(gh release list --repo "$REPO" --limit 1 --json tagName --jq '.[0].tagName')
echo "Latest release: $LATEST"
# List commits since last release
git log $LATEST..HEAD --oneline
# Get detailed changes
git log $LATEST..HEAD --pretty=format:"- %s (%h)"Read the commit list and diff directly. Trace user-facing changes to their changelog entries and classify them as features, bug fixes, breaking changes, or documentation. Ignore internal refactors unless they alter behavior.
| Type | When | Example |
|---|---|---|
| PATCH (x.x.X) | Bug fixes, docs, deps | 2.19.1 |
| MINOR (x.X.0) | New features, backward compatible | 2.20.0 |
| MAJOR (X.0.0) | Breaking changes | 3.0.0 |
Obtain the mandatory independent review of breaking-change and resource- recreation risk. Give the reviewer the exact release diff and require a final verdict with concrete file and line references; verify every finding against the code and upgrade-plan evidence.
## [Unreleased]
### ⚠️ Upgrade Notes
<!-- Migration guides, breaking change warnings, special upgrade steps -->
### 🚀 New Features
<!-- New functionality added -->
### 🐛 Bug Fixes
<!-- Bugs that were fixed -->
### 🔧 Changes
<!-- Non-breaking changes, refactors, improvements -->
### 📚 Documentation
<!-- Documentation updates -->(#1234)### 🚀 New Features
- **K3s v1.35 Support** - Added support for k3s v1.35 channel (#2029)
- **NAT Router IPv6** - NAT router now supports IPv6 egress (#2015)
### 🐛 Bug Fixes
- Fixed autoscaler not respecting max_nodes limit (#2018)
- Resolved firewall rules not applying to new nodes (#2012)
### ⚠️ Upgrade Notes
- **NAT Router users**: Run `terraform apply` twice after upgrade due to route changesUpdate README.md badges if version references changed:
[](https://k3s.io)Check versions.tf for:
If significant changes, regenerate the machine-readable knowledge file (Custom GPT retired 2026-07-13; this feeds future tooling such as the planned MCP server):
meta.version to the release being prepared.Do not invent a checked-in generator path if one is not present in the worktree.
Normal path: the release notes draft is the CHANGELOG.md content under ## [Unreleased]. Make sure it contains the target release section, issue/PR references, upgrade notes if any, and no stale placeholder text.
Preview exactly what the workflow will extract:
awk '/^## \[Unreleased\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.mdIf a separate release-notes file exists in a future train, use it as a drafting aid, but copy the final release content into CHANGELOG.md under ## [Unreleased] before tagging so the automation can consume it.
Before tagging a v3 release, run the local readiness gates:
terraform fmt -recursive
terraform-docs markdown table --config .terraform-docs.yml --output-mode inject --output-file docs/terraform.md .
terraform init -backend=false -input=false
terraform validate -no-color
tmpdir="$(mktemp -d)"
rsync -a --exclude .git --exclude .terraform --exclude .terraform-tofu ./ "$tmpdir"/
(cd "$tmpdir" && tofu init -backend=false -input=false && tofu validate -no-color)
rm -rf "$tmpdir"
uv run scripts/validate_tailscale_large_scale_examples.py
uv run scripts/validate_v3_final_polish_examples.py
uv run scripts/smoke_v3_plan_matrix.py
scripts/tests/test_github_release_controls_contract.sh
scripts/check-github-release-controls.sh
scripts/tests/test_create_distribution.sh
scripts/tests/test_generated_site_contract.shThe GitHub control check is a live, authenticated pre-release gate. It verifies
that the default branch requires pull requests plus the cheap lint check and
blocks force-push/deletion, v* tags have an active creation/update/deletion/
non-fast-forward ruleset, and no GitHub-hosted HCloud environment remains. A
repository fixture cannot substitute for these live settings.
Keep setup simple: the README exposes the canonical createkh one-liners while
scripts/create.sh owns source distribution, manifest verification, and atomic
Packer bundle publication. Test the downloaded-script path and the checked-out
source path before release. A specific release can be selected with
KH_SOURCE_REF=vX.Y.Z. Exceptional independently verified remote builds must
set all three strict pins together: KH_SOURCE_COMMIT (full 40-character SHA),
KH_SOURCE_ARCHIVE_SHA256, and KH_PACKER_BUNDLE_MANIFEST_SHA256. Strict pins
cannot be combined with KH_SOURCE_DIRECTORY and are not part of normal release
choreography.
Cross-variable and local-dependent module contract failures are hard
terraform_data.validation_contract preconditions, so invalid-combination
release gates must assert terraform plan; terraform validate only proves
the module loads.
Also verify README.md, kube.tf.example, docs/llms.md, and .claude/skills/*/SKILL.md do not contain removed v2 input names except in explicit migration sections.
For v3 releases, verify README's compact "Current release:" URL points to the
latest tag and CHANGELOG.md carries the release content without stale
pre-release wording. Regenerate site-docs and run
scripts/tests/test_generated_site_contract.sh; the site must reproduce the
same simple setup commands as the README.
For v3, additionally verify the Tailscale node-transport surfaces stay aligned:
node_transport_mode = "tailscale" is the supported secure single-network and
private multinetwork path, Flannel is first supported, Cilium is experimental,
Calico is rejected, subnet-route SNAT is disabled when advertising routes,
single-network examples may disable node-private route advertisement, and
active Tailscale agent/autoscaler nodepools set network_scope explicitly so
same-root external Network IDs are validated during terraform plan,
external-overlay docs still describe only user-owned operator
access/post-bootstrap features.
Also verify the final v3 topology polish surfaces stay aligned:
docs/v3-topology-recommendations.md, examples/cilium-gateway-api,
examples/external-overlay-cloudflare-access, cilium_gateway_api_enabled,
embedded_registry_mirror, endpoint outputs,
public join endpoint IPv6/no-public-host guards, OpenTofu/null-resource gates,
and the large-scale Tailscale examples must all match variables.tf,
locals.tf, kube.tf.example, and docs/llms.md.
For Cloudflare, keep the release support boundary sharp: Access/Tunnel is a documented external access pattern for operator/app endpoints; kube-hetzner does not manage Cloudflare provider resources, and Cloudflare Mesh/WARP is not a v3 node-transport support promise.
For the required cheap PR lint, require a completed success. GitHub Actions do not run HCloud plans, applies, cluster inspection, or destroy. Run those gates locally from the maintained kube-test roots and retain their logs before tag publication.
## 🚀 Release vX.Y.Z
### Highlights
- **Feature 1**: Brief description
- **Feature 2**: Brief description
### ⚠️ Upgrade Notes
[Any special upgrade instructions]
### What's Changed
#### New Features
- Feature description (#PR)
#### Bug Fixes
- Fix description (#PR)
#### Other Changes
- Change description (#PR)
### Full Changelog
https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner/compare/vPREV...vX.Y.Z
### Upgrade
\`\`\`tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "X.Y.Z"
# ...
}
\`\`\`
\`\`\`bash
terraform init -upgrade
terraform plan
terraform apply
\`\`\`git status --short
git add CHANGELOG.md README.md docs/llms.md docs/terraform.md kube.tf.example .claude/skills
git commit -m "$(cat <<'EOF'
chore: prepare release vX.Y.Z
- Update release notes and version references
EOF
)"
git push origin HEAD
gh pr create --base master --head "$(git branch --show-current)" --fillRelease preparation and post-release cleanup always use pull requests because
master protection applies to administrators. Never bypass or temporarily
weaken that protection for a release.
VERSION=vX.Y.Z
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
git checkout master
git fetch --no-tags origin master
git merge --ff-only refs/remotes/origin/master
test -z "$(git status --porcelain)"
RELEASE_COMMIT=$(git rev-parse HEAD)
# Refuse to continue if either command prints the tag.
git tag --list "$VERSION"
git ls-remote --tags origin "refs/tags/$VERSION"
# Immediately before tagging, re-prove live controls and refresh the authoritative
# branch. The atomic push lease prevents the tag from publishing if master moves
# after this fetch.
test -z "$(git status --porcelain)"
scripts/check-github-release-controls.sh
git fetch --no-tags origin master
scripts/check-release-ref-on-remote-default-branch.sh --require-tip "$RELEASE_COMMIT" refs/remotes/origin/master
test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT"
# Create and atomically push the tag with the unchanged protected-master tip.
# If this push fails, no remote tag is created; delete the local tag before retrying.
git tag -a "$VERSION" "$RELEASE_COMMIT" -m "Release $VERSION"
git push --atomic --force-with-lease="refs/heads/master:$RELEASE_COMMIT" origin \
"${RELEASE_COMMIT}:refs/heads/master" "refs/tags/$VERSION"
# Confirm the lightweight publication workflow and release.
gh run list --repo "$REPO" --workflow "Publish a new Github Release" --limit 1
gh release view "$VERSION" --repo "$REPO"If publication fails, do not move the immutable tag. Re-run all local release
gates against the tag target, including create distribution, generated docs,
the authoritative remote-branch check, and live GitHub controls. Regenerate the
exact release body from that tag's [Unreleased] section, confirm no partial
release exists, then use
gh release create "$VERSION" --title "$VERSION" --notes-file <file> --generate-notes.
A pushed release tag is immutable. If the workflow definition at that tag has a permanent defect, do not move or recreate the tag: manually create the release from that tag's reviewed release content only after the local gates above, verify the public body and tag target, then repair the cheap publication workflow through a post-release pull request.
VERSION=vX.Y.Z
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh release view "$VERSION" --repo "$REPO" --json tagName,name,isPrerelease,publishedAt,url,targetCommitish
gh release list --repo "$REPO" --limit 3
git ls-remote --tags origin "refs/tags/$VERSION"Inspect the live release notes, not just the workflow status:
gh release view "$VERSION" --repo "$REPO" --json body --jq .bodyIf the body contains stale content from older releases, edit the GitHub release directly with a corrected body and then fix CHANGELOG.md on master so the same mistake does not recur.
If creating or updating a pinned upgrade notice issue after release, write it for the full practical upgrade path users need, not just the latest patch delta. For example, after a v2.19.x patch, the pinned notice should cover upgrading from v2.18.x to the current v2.19.x, including older release caveats such as state migration instructions, version requirements, and plan-review warnings.
After confirming the live release, cut the changelog:
## [Unreleased]
_No unreleased changes._
---
## [X.Y.Z] - YYYY-MM-DD
...released notes...Create a release-maintenance branch for the changelog cut, push it, and merge
it through a pull request with the same protected-master review flow.
Files that may need version updates:
| File | What to Update |
|---|---|
README.md | Badge versions |
CHANGELOG.md | Release content must stay under [Unreleased] until tag publication runs |
docs/llms.md | Example version references |
kube.tf.example | Version in comments |
.claude/skills/*/SKILL.md | Operator workflows, v3 migration names, validation gates |
| GPT knowledge | meta.version |
git log <prev-tag>..HEAD --format='%an <%ae>' | sort -u includes every community submitter whose work ships in this releasemergedAt; indirectly integrated PR heads are ancestral to the release targetdocs/terraform.md regeneratedcreatekh setup commands as READMEkube-hetzner-knowledge.jsondata) regenerated when applicable and meta.version matches the releaseuv run scripts/validate_v3_final_polish_examples.py passeduv run scripts/smoke_v3_plan_matrix.py passed for Gateway API, registry mirror, public join endpoint guards, k3s/RKE2 Tailscale multinetwork constraints, and single-Gateway-controller validationscripts/tests/test_create_distribution.sh passed for local and downloaded-script source modesCHANGELOG.md is cut after release, with a clean [Unreleased] sectionbb1622c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.