Release a milestone
From a milestone to a release on main: cut release/next, collect the work in a milestone, deliver it, review it and let a person merge the release PR. Each step is one prompt.
Prerequisites
gh auth statusworks, a cleangit status, run from the repository root.- The ruleset on
release/nextbelow. It is the hard gate that/deliverchecks for.
The path
1. Cut release/next
Cut a release.github-release cut creates the rolling release/next from main. A branch that holds unreleased work is never reset; a released one is moved to main by a fast-forward; a fresh one is left as it is. PBI pull requests target this branch. Rules for the branch types and merge methods: references/release-branches.md of github-issue.
2. Create and inspect the milestone
To spread many open issues over several releases first, plan them with /roadmap; it sets the M<nnn> milestones and keeps ROADMAP.md.
Milestones are the releases. Name them M<nnn> or M<nnn> <Topic> (nnn a three-digit number, M001 to M999; optional topic, no version; M003 Market data; the lookup accepts M11 for M011, and create -Name M15 creates M015); on close the version is added at the end (M003 Market data (x.y.z)). The number sets the order, the topic is ignored for it and M003 finds M003 Market data. Existing x.y.z titles stay valid.
To put a milestone between two open ones, use a sub-number: /github-milestone create M001.1 sorts between M001 and M002 (numerically per part: 1, 1.1, 1.2, 2, 10), M001 never finds M001.1, and current stays the lowest open one. For a clean sequence use /github-milestone insert M002 --dry-run, then /github-milestone insert M002: every open M<kkk> with k ≥ 2 (and its sub-numbered ones) moves up by one, from the highest down, then M002 is created. Closed milestones are never renamed (M001 (0.1.0) carries its version, nothing can come before it). insert lists what it does not rewrite: old names in issue bodies, ROADMAP.md and release-review-M<n>.json state files; you decide. deliver uses the sub-number as state key (M001.1, not M001).
/github-milestone create M003
/github-milestone currentcurrent is the lowest open M<nnn> (a legacy x.y.z milestone above the latest v<x.y.z> tag comes first). It lists its open issues grouped as ready, blocked, needs-decision, needs-refinement and claimed, plus the release PR. Assign the planned PBIs and Bugs to the milestone before you deliver: /github-milestone assign M3 #12 #13 (none clears it). Set milestones only this way, never with gh issue edit --milestone.
3. Deliver the milestone
/deliver --milestone M003Every open PBI and Bug of the milestone is delivered into release/next (deliver guide). For long milestones use fresh sessions per PBI (loop).
4. Release review
When nothing is left, deliver reviews the whole diff origin/main...origin/release/next against the criteria of the closed issues. Blocking findings become Bug issues in the milestone and no release PR is opened: run /deliver --milestone M003 again. Without blocking findings, up to five notes go into the release PR. Details: delivery reference.
5. Finish: release PR and changelog
deliver runs finish for a fully delivered milestone. By hand:
Finish the release with an updated changelog.github-release finish opens the release PR from release/next into main (merge commit, never squash, so the feat: and fix: commits stay visible to GitVersion and git-cliff). It refuses when the branch is behind main, has nothing to release, was already released or has open PRs against it. finish major adds a deliberate major bump.
Changelog
On first use finish installs git-cliff, creates cliff.toml from its template and adds a changelog script (references/cliff-setup.md of github-release). It does not edit CHANGELOG.md: the release job on main regenerates it with git-cliff, tags the version, publishes it and closes the issues of the release's PRs. Commit titles produced by github-issue and github-pull-request already follow the conventional-commit format git-cliff expects. /github-release help prints the usage.
6. A person merges into main
Review the release PR, then merge it with a merge commit:
gh pr merge <nr> --mergedeliver and finish never merge it and never wait for it. release/next is never deleted. After the merge the next github-release finish (or cut) moves it to main by a fast-forward for the next release, and open PRs into it keep their base.
After a hotfix
A hotfix goes straight into main and must come back into release/next, otherwise finish stops (main is not an ancestor of release/next; Get-ReleaseState.ps1 reports defaultIsAncestor and mergeConflicts). The back-merge is a real merge commit, not a squash, which would drop the ancestry:
- Branch from
release/next, mergeorigin/main, take the content ofrelease/nexton conflicts. - Open a PR into
release/nextwithCloses #<issue>in the body. - Allow the
mergemethod in the ruleset for this one PR, merge withgh pr merge <nr> --merge, set the ruleset back tosquashonly.
The skill never changes the ruleset; the decision is per back-merge, not a permanent merge method. Details: back-merge section of github-release.
Ruleset on release/next
/deliver squash merges without asking when the review is clean and the checks are green. The ruleset is what makes that safe: a red or missing required check blocks the merge whatever the session does. At the start deliver runs
gh api repos/{owner}/{repo}/rules/branches/release/next --jq '.[].type'and warns when required_status_checks is not in the output.
Settings, in the UI under Settings > Rules > Rulesets > New ruleset > New branch ruleset:
- Name
release/next, enforcement Active. - Bypass list: the repository admin role only, mode always.
github-releasepushes the fast-forward ofrelease/nextas the owner; no bypass for apps or other roles. - Target branches: add target, include exactly
refs/heads/release/next(arelease/**pattern would also coverrelease/<x.y>maintenance branches). - Require a pull request before merging, allowed merge methods Squash only (PBI branches squash into the release branch).
- Require status checks to pass: add your CI checks by name; leave "Require branches to be up to date" off unless you want a rebase per PBI.
- Block force pushes on. Turn Restrict deletions on:
github-releasemovesrelease/nextby a fast-forward and never deletes it, and without the rule "Automatically delete head branches" deletes it when the release PR is merged and GitHub moves every open PR intorelease/nexttomain(#528).Get-ReleaseState.ps1warns when the rule is missing.
The same through the API (replace build with your check names; integration_id is optional):
gh api repos/{owner}/{repo}/rulesets --method POST --input - <<'JSON'
{
"name": "release/next",
"target": "branch",
"enforcement": "active",
"bypass_actors": [ { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" } ],
"conditions": { "ref_name": { "include": ["refs/heads/release/next"], "exclude": [] } },
"rules": [
{ "type": "non_fast_forward" },
{ "type": "deletion" },
{ "type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["squash"] } },
{ "type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [ { "context": "build" } ] } }
]
}
JSONThe main branch has its own ruleset that allows merge commits only, for the release PR. Check the result with the gh api .../rules/branches/release/next call above.
Next
Deliver issues, delivery reference.
7. Close the milestone
Once the release job has tagged v1.3.0, close the milestone and create the next one. The next github-release finish (the run that deletes the merged release/next) prints this hint for the lowest open M<nnn>, with the version taken from the tag:
/github-milestone create M004
/github-milestone close M003With open issues left, create the next milestone first and move them: /github-milestone create M004, then /github-milestone close M003 --move-to M004. --move-to needs the target to exist and be open, so create always comes before close ... --move-to; create is idempotent. Legacy repositories whose open milestone is named x.y.z get the old hint: create the next x.y.z milestone first, then /github-milestone close 1.3.0 [--move-to <next>].
close needs the release tag: without --version <x.y.z> it takes the newest tag not yet assigned to a milestone, and renames M003 to M003 (1.3.0) (M003 Market data to M003 Market data (1.3.0)) while closing it. Open issues stay, and nothing is renamed or closed, unless you pass --move-to M<nnn>, an existing open milestone that receives them (create it first). Pull requests are not moved. A milestone that is already closed is left as is. close renames only to add the version, never edits the due date or deletes a milestone. To rename a legacy x.y.z milestone (or any open one) use /github-milestone rename <name> <new title>: rename 0.2.0 "M002 (0.1.1)" (the version comes from the new title, the number of an M<nnn> never changes, closed milestones are refused).
Migrate legacy milestones
A repository with x.y.z milestones moves to M<nnn> in one pass; no flag day is needed, legacy titles keep working meanwhile. Worked example: tamias, whose milestone 0.2.0 shipped as v0.1.1. The number follows the order of the releases, the version in the parentheses is the tag that shipped. A repository with short numbers (M1, M11) migrates its titles once with /github-milestone renumber --dry-run, then pwsh scripts/Get-Milestones.ps1 -Mode renumber -DryRun, and -Mode renumber after approval (-Repo o/r for one repository, or -AllRepos -Owner <owner> for every non-archived repository of the owner); renumber prints renames[] (number, state, old, new), skipped[] and summary; old titles in issue bodies and ROADMAP.md are not rewritten (only insert lists them, in references[]), so update those by hand.
| Legacy title | New title | State |
|---|---|---|
0.1.0 | M001 (0.1.0) | shipped, close |
0.2.0 | M002 (0.1.1) | shipped as v0.1.1, close |
0.3.0 | M003 | open |
0.4.0 | M004 | open |
- Rename every milestone, shipped or open:
/github-milestone rename 0.1.0 "M001 (0.1.0)",/github-milestone rename 0.2.0 "M002 (0.1.1)",/github-milestone rename 0.3.0 M003,/github-milestone rename 0.4.0 M004.renametakes open milestones only, so rename before you close; the version comes from the new title. The rename of0.3.0makes the targetM003exist for step 2; do notcreateit first, a taken title is refused. - Close the shipped milestones and move their open issues:
/github-milestone close M001 --version 0.1.0and/github-milestone close M002 --version 0.1.1 --move-to M003. The target must exist and be open beforeclose --move-to(#226), so the renames come first; without--move-toa milestone with open issues stays open and unchanged. - Check with
/github-milestone list, then create the next milestone with/github-milestone create M005when work starts.
deliver keeps its state in release-review-<title>.json (<git-common-dir>/.agents/state/). The files of the old titles (release-review-0.3.0.json) stay as history and are not read again; new runs use the unpadded M<n> key (M003 is M3: release-review-M3.json), the same before and after close.
Also update the places of the migrated repository that name a milestone by version. In tamias these are the headings of docs/roadmap.md (## 0.3.0 becomes ## M003), plus any --milestone 0.3.0 in scripts, notes or issue templates; run /deliver --milestone M003 from then on.