Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions skills/drafting-a-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Work the steps in order. Steps 1, 3, 5, and 6 are gates — do not run ahead of

If they choose a specific commit, accept whatever form they give it — a commit SHA, any resolvable ref (a tag or a branch), or a description of intent ("the commit where the X landed", "everything up to PR #N"). Resolve a description to one concrete commit from `git log` and the merged-PR history, then echo back the short SHA, subject, and date and wait for an explicit yes before using it. Confirm the resolved commit even when they hand you a literal SHA — show its subject so they can catch a typo or a stale paste.

The resolved commit (or the branch tip, if that's what they chose) is `<target>` for the rest of the run: it sets the upper bound of the next step's range and the `--target` of the publish.
The resolved commit (or the branch tip, if that's what they chose) is `<target>` for the rest of the run: it sets the upper bound of the next step's range and the `--target` of the publish. The short SHA you echo here is for the human-readable confirmation only — when `<target>` reaches `--target`, pass the full 40-char SHA (`git rev-parse <target>`) or the branch name, never the abbreviated form. The publish step says why.

2. **Establish the baseline.** Find the last release and what shipped between it and the release point: `gh release list` (or `git describe --tags --abbrev=0`) for the last tag, then `git log <last-tag>..<target>` and the merged PRs in that range. Bounding the range at `<target>` rather than HEAD keeps anything past the release point out of the notes. This range is the raw material for both the curated sections and the changelog.

Expand All @@ -36,33 +36,33 @@ Work the steps in order. Steps 1, 3, 5, and 6 are gates — do not run ahead of

5. **Present the full body and proposed tag, and let the user refine.** Paste the complete drafted body and the tag in chat. Invite edits and iterate until they're happy. Do not move to publishing off a body they haven't seen in full.

6. **Ask whether it's a draft or a published release.** This is the user's choice — ask it explicitly, don't default. A draft (`--draft`) stages the notes on GitHub for a final human look without going live; omitting the flag publishes immediately. If they're unsure, recommend draft (it's the reversible option) but let them decide.

7. **Confirmation gate, then create.** See below.
6. **Publish gate — draft or published, and that choice is the go-ahead.** With the full body already shown in step 5, ask the one question that both picks the mode and authorizes the publish: draft or published? A draft (`--draft`) stages the notes on GitHub for a final human look without going live; omitting the flag publishes immediately. If they're unsure, recommend draft (it's the reversible option) but let them decide. Their answer — made against this exact tag, body, and commit — is the explicit consent to create, so run `gh release create` on it. Do not stack a second "Confirm?" prompt on top of a publish-mode choice they just made. See below for what this single gate must put in front of them.

## Core principle: user-in-the-loop for the publish

Don't run `gh release create` without an explicit confirmation **for the exact release about to land**. A "yes, cut a release" at the start of the task is intent to begin, not approval of this tag, this body, and this draft/published state.
Don't run `gh release create` on inferred consent. A "yes, cut a release" at the start of the task is intent to begin, not approval of this tag, this body, and this draft/published state. The explicit consent is the user's draft-or-published choice (step 6) made against the exact release you've laid out. That single choice is the go-ahead: once they've made it, create — don't stack a separate "Confirm?" prompt on top of it.

The confirmation you show the user spells out all four:
For that choice to count as consent, the gate must put the whole release in front of them at once:

- The **tag / version**, and the **commit it lands on** — the resolved short SHA when a specific commit was chosen, or the default branch tip.
- **Draft or published.**
- The **full notes body**.
- The **tag / version**, and the **commit it lands on** — the short SHA for readability when a specific commit was chosen, or the default branch tip.
- The **full notes body** — shown in full in step 5; restate it or point to it directly above, so they're choosing against what they can see.
- The **repo**, if there's any chance of ambiguity about which one.
- The **draft-or-published choice itself** — the action they take to consent.

Phrase it as: "About to create release `<tag>` on `<repo>` as **<draft|published>** with the body below. Confirm?" — then paste the body and wait for an explicit yes. Treat absence of objection as a no.
Phrase it as: "Release `<tag>` on `<repo>`, landing on `<commit>`, body above — publish it live now, or stage it as a draft?" Their answer is the consent; act on it. Treat silence or a non-answer as a no.

Once confirmed:
Then create:

```bash
gh release create <tag> \
--title "<tag>" \
--notes-file <body-file> \
[--target <ref>] \
[--target <full-sha-or-branch>] \
[--draft]
```

`--target` takes a **branch name or the full 40-char commit SHA — never an abbreviated SHA.** GitHub's release API rejects an abbreviated `target_commitish` and reports it as `tag_name is not a valid tag` / `Release.target_commitish is invalid`, which reads like a tag problem but is the short SHA. Resolve it with `git rev-parse <target>`. Omit `--target` entirely only when cutting from the default branch's current tip (the API's default).

## Anti-patterns

- **Tagging the branch tip because no commit was named.** Where the release is cut from is a gate, like the version. Ask whether it's the branch tip or a specific commit; don't read silence as HEAD.
Expand All @@ -72,7 +72,9 @@ gh release create <tag> \
- **Burying a breaking change** in the middle of a list. It goes first, flagged.
- **Picking the version yourself** and moving on. Propose, then let the user decide.
- **Defaulting draft-vs-published** instead of asking. The user owns that call.
- **Running `gh release create` on inferred consent.** Every release is a fresh confirmation against the specific tag, body, and draft state.
- **Running `gh release create` on inferred consent.** A start-of-task "cut a release" is not approval of this tag, body, and state. The user's draft-or-published choice against the shown body is that approval — get it before creating.
- **Re-confirming after the user already chose.** Once they've picked draft or published against the body you showed, that *is* the consent. A second "Confirm?" prompt on top of it is friction, not safety — create.
- **Passing an abbreviated SHA to `--target`.** GitHub rejects it as an invalid `target_commitish`, surfacing as "tag_name is not a valid tag". Use the full 40-char SHA (`git rev-parse`) or a branch name.

## Red flags: STOP before running `gh release create`

Expand All @@ -84,10 +86,11 @@ These thoughts mean the release isn't ready to publish:
| "I'll default to a published release since they want it live" | Draft-vs-published is the user's choice. Ask, don't assume. |
| "I'll just default to draft to be safe and publish later" | Still the user's call. Recommend draft if unsure, but ask. |
| "Breaking change is in there, the reader will see it" | They skim. Put it first, flag it loudly, give the migration. |
| "They said cut a release a minute ago, that's a yes" | That's intent to start. Confirm the exact tag, body, and draft state now. |
| "They said cut a release a minute ago, that's a yes" | That's intent to start. The go-ahead is their draft-vs-published choice made against the body you've shown. |
| "Semver is obvious, I'll just tag it" | Propose the bump with reasoning; the version is the user's to confirm. |
| "They didn't name a commit, so cutting from HEAD is fine" | Where the tag lands is a gate. Ask: the branch tip, or a specific commit? |
| "The top commit is obviously WIP — I'll target the one below" | Don't infer the release point. Ask, then confirm the resolved commit before using it. |
| "They gave me a SHA, so I don't need to confirm it" | Echo its subject and date — a typo or stale paste tags the wrong commit. |
| "I'll pass the short SHA I echoed back as `--target`" | GitHub rejects an abbreviated `target_commitish` ("tag_name is not a valid tag" is the misleading symptom). Pass the full 40-char SHA or a branch name. |

All of these mean: finish the curated body, present tag + body + draft state, and wait for an explicit yes.
All of these mean: finish the curated body, present tag + body + commit, and let the user's draft-or-published choice against it be the explicit go-ahead.