name: cut-release description: > Interactive walkthrough for cutting a new release of Huntable CTI Studio. Use this skill whenever the user says "cut a release", "ship a release", "tag a version", "bump the version", "new release", "do the release", "release vX.Y.Z", "ship v5.4.0", "time to release", or otherwise signals they want to move code from the release branch to main and publish a tagged GitHub Release. Drives scripts/release_cut.py plus the branch unlock/lock dance and the tag push that triggers .github/workflows/release.yml, pausing at every irreversible step so the operator can confirm.
Cut Release
Walks the operator through the release flow documented in AGENTS.md
(sections "Release branch protection" and "Release tagging convention").
Those sections are authoritative; this skill automates the mechanics and
enforces the checkpoints.
Core invariants
Hold these in mind throughout:
mainis read-only between releases via GitHub branch protection (lock_branch,enforce_admins, no force push, no deletions). Feature work lives on the release branch — the codename/version-named line, currentlyeuropa-dev(scripts/release_cut.pyaccepts any branch matchingeuropa-*or the historicaldev-europa*, and rejects anything else).mainonly moves during release cuts.- Canonical tag format is
vMAJOR.MINOR.PATCHonly. The codename lives in the annotated tag message and the CHANGELOG heading, never in the tag name. pyproject.toml[project].versionis the single source of truth. Everything else mirrors it.- Never push silently. Every network-side-effect step (unlock, push the release branch, push tag, relock) gets an explicit operator confirm.
Phase 1: Gather release parameters
Ask the operator for three things. Confirm all three before touching anything.
Version in
MAJOR.MINOR.PATCHform. Read the current version frompyproject.toml[project].versionand help decide the bump type:- Patch (
5.3.0 -> 5.3.1): bug fixes only. - Minor (
5.3.0 -> 5.4.0): new features, backward compatible. - Major (
5.3.0 -> 6.0.0): breaking changes or significant architectural shift.
- Patch (
Codename:
- Major bump: pick a new planetary moon. Show the operator the
"Available Planetary Moon Names" section in
docs/reference/versioning.md, and exclude names already used in the Version History block (historically: Callisto, Ganymede, Kepler, Copernicus, Tycho). Good fresh candidates: Triton, Europa, Io, Titan, Enceladus. - Minor or patch bump: reuse the current codename. Read it from the
first line under
## Current Versionindocs/reference/versioning.md.
- Major bump: pick a new planetary moon. Show the operator the
"Available Planetary Moon Names" section in
One-line summary for the annotated tag message. Short, declarative. This goes into
git tag -mand is whatgit log --onelineshows next to the tag. Example:"SBOM provenance + release automation".
This is the last easy exit point. After Phase 3 commits land, backing out gets fiddly.
Phase 2: Preflight
Run these checks via Bash. Halt with a clear diagnostic on the first failure.
git rev-parse --abbrev-ref HEAD # must match europa-* or dev-europa* (e.g. europa-dev)
git status --porcelain # must be empty
git fetch origin "$(git rev-parse --abbrev-ref HEAD)"
git rev-parse HEAD # must equal the FETCH_HEAD sha
git rev-parse FETCH_HEAD
Also check that docs/CHANGELOG.md [Unreleased] contains real content
(at least one ### subsection between the ## [Unreleased] heading and
the next ## [ heading). If [Unreleased] is empty, there is nothing to
release -- abort and tell the operator.
scripts/release_cut.py repeats these preflight checks, but catching
failures earlier keeps the flow tight.
Phase 2b: Security Review
Before touching the repo, invoke the built-in security review skill:
/security-review
This scans the diff between the release branch and main for common vulnerability
classes (injection, auth gaps, exposed secrets, insecure deserialization,
etc.). Review every finding. You have two options:
- Fix and commit on the release branch, then loop back to Phase 2 preflight to re-confirm the branch is clean and up-to-date.
- Accept the risk with an explicit operator decision. Document the accepted risk in a follow-up commit or the CHANGELOG before proceeding.
Do not proceed to Phase 3 until all findings are resolved or explicitly accepted.
Phase 3: Run release_cut.py
Execute:
scripts/release_cut.py <version> <Codename> --summary "<summary>"
This does every repo-local edit atomically, does NOT push, and stops after creating the local commit and annotated tag. Specifically:
- Bumps
pyproject.toml[project].version. - Rolls
docs/CHANGELOG.md[Unreleased]into a dated[X.Y.Z "Codename"] - YYYY-MM-DDsection, inserting a fresh empty[Unreleased]above. - Shifts
docs/reference/versioning.mdCurrent/Previous/Earlier labels and inserts a Version History stub entry (with TODO markers for the operator to fill in later). - Updates the version line in
README.md. - Runs
scripts/verify_release_tag.py vX.Y.Zas a pre-flight guard. - Commits as
release: vX.Y.Z "Codename". - Creates annotated tag
vX.Y.Zwith the operator's summary.
If the script fails, read the error first before retrying. Common causes:
[Unreleased]empty.versioning.mdCurrent Version block does not match the canonical three-line shape.- Tag already exists locally (
git tag -d vX.Y.Zto clear, then rerun).
If edits are on disk but commit did not happen, git diff to review and
git restore . to back out cleanly.
Phase 4: Review the commit and tag
Show the operator:
git show HEAD --stat
git show vX.Y.Z
Ask for explicit approval to continue. This is the last cheap-exit point. Backing out at this stage:
git tag -d vX.Y.Z
git reset --hard HEAD~1
Things to look at before approving:
pyproject.tomlversion matches the intended target.docs/CHANGELOG.mdhas the new dated section and a fresh empty[Unreleased].docs/reference/versioning.mdCurrent Version block shifted correctly, and a new Version History entry exists. The operator may want to fill in the Significance / Features TODOs on a follow-up commit before merging to main. Do not amend the release commit on the operator's behalf.
Phase 5: Unlock main
scripts/release_unlock.sh
Removes branch protection on main. From this point on, main is
write-enabled and you should move through the remaining phases without
long pauses. Do not leave main unlocked overnight.
Phase 6: Push the release branch and open PR
git push origin "$(git rev-parse --abbrev-ref HEAD)" # the release branch; release_cut.py prints this exact command too
Direct the operator to open a PR manually on GitHub:
- Base:
main - Compare: the release branch (e.g.
europa-dev) - Title:
release: vX.Y.Z "Codename"(match the commit subject)
Wait for the operator to report the PR is open. Then wait for all CI checks to go green. If CI fails, stop -- fix on the release branch, push a follow-up commit, re-check. Do not merge with red CI.
Phase 7: Merge the PR
The operator merges the PR on GitHub using a merge commit (not squash, not rebase). Reason: the annotated tag points at a specific commit SHA. Squash would rewrite that SHA out of existence and detach the tag from main.
Wait for the operator to confirm the PR is merged. Optionally update local main:
git fetch origin
Phase 8: Push the tag
git push origin vX.Y.Z
This triggers .github/workflows/release.yml, which:
- Re-runs
scripts/verify_release_tag.pyagainst the pushed tag. - Extracts the
[X.Y.Z]section fromdocs/CHANGELOG.mdviascripts/extract_changelog_section.py. - Creates a GitHub Release titled
vX.Y.Z "Codename"with the CHANGELOG section as the body.
Give the operator the Actions URL and wait for the workflow to complete:
https://github.com/dfirtnt/Huntable-CTI-Studio/actions
If the workflow fails, see references/recovery.md section "Tag pushed
but release.yml rejected it". Do not proceed to Phase 9 until the Release
is published.
Phase 9: Relock main
scripts/release_lock.sh
Restores the read-only lock on main. The release flow is complete.
Phase 10: Sanity checks
Walk the operator through these:
- GitHub Release visible at
https://github.com/dfirtnt/Huntable-CTI-Studio/releases/tag/vX.Y.Z. - Release notes body matches the
[X.Y.Z]CHANGELOG section. - Release title includes the codename.
- Tag visible remotely:
git ls-remote --tags origin | grep vX.Y.Z. mainis locked: a test push (git push origin main --force-with-leasefrom a scratch branch) must be rejected. Skip this if the operator does not want to exercise it.
Recovery and special cases
See references/recovery.md for:
release_cut.pyfailed mid-run.- Tag pushed but release.yml rejected it.
- Commit accidentally landed on main outside the flow.
- Need to yank a published release.
Scope limitations
This skill covers standard patch / minor / major releases. Release
candidates (vX.Y.Z-rc.N on a release/vMAJOR branch) need additional
steps beyond this walkthrough. See AGENTS.md "Release tagging convention"
for the RC pattern. RC flow is not yet covered here -- when you hit that
case, walk through manually and consider extending this skill.