|
| 1 | +# Skill: Release apiops-cli Version |
| 2 | + |
| 3 | +**Confidence:** high |
| 4 | +**Scope:** Any agent (or human) cutting a new release of `@peterhauge/apiops-cli` |
| 5 | + |
| 6 | +## What |
| 7 | + |
| 8 | +End-to-end checklist for releasing a new version of apiops-cli. Walks through cleanliness checks, branch creation, changelog authoring, version bumping with `npm version`, and PR creation. |
| 9 | + |
| 10 | +## Why |
| 11 | + |
| 12 | +The release workflow (`.github/workflows/squad-release.yml`) **fails the build** if `package.json` `version` is not headed in `CHANGELOG.md` as `## [VERSION]`. Releases that skip the changelog never tag. This skill keeps version, changelog, and tag in lockstep so consumers can always answer "what changed since the version I have installed?" by reading one file. |
| 13 | + |
| 14 | +## How |
| 15 | + |
| 16 | +Follow these steps in order. Do not skip the cleanliness check — switching branches with a dirty tree loses work. |
| 17 | + |
| 18 | +### 1. Confirm the working tree is clean |
| 19 | + |
| 20 | +```powershell |
| 21 | +git status --short |
| 22 | +``` |
| 23 | + |
| 24 | +- If output is empty (or only contains `??` untracked files you've verified are safe to leave behind), you can proceed. |
| 25 | +- If there are modified or staged tracked files, **STOP**. Tell the user: |
| 26 | + > "There are uncommitted changes in the working tree. Please commit, stash, or discard them before starting a release. Run `git status` to see what's pending." |
| 27 | +
|
| 28 | +Do not run `git stash`, `git reset`, or `git checkout -- .` on the user's behalf. |
| 29 | + |
| 30 | +### 2. Switch to main and sync |
| 31 | + |
| 32 | +```powershell |
| 33 | +git checkout main |
| 34 | +git pull --ff-only origin main |
| 35 | +``` |
| 36 | + |
| 37 | +If the pull fails (non-fast-forward), STOP and ask the user how to reconcile. |
| 38 | + |
| 39 | +### 3. Create the release branch |
| 40 | + |
| 41 | +Use a predictable name so reviewers know it's a release PR: |
| 42 | + |
| 43 | +```powershell |
| 44 | +git checkout -b release/v<NEW_VERSION> |
| 45 | +# e.g. git checkout -b release/v0.3.1-alpha.0 |
| 46 | +``` |
| 47 | + |
| 48 | +Don't commit the version bump yet — the changelog has to be written first (Step 6) so it lands in the same commit. |
| 49 | + |
| 50 | +### 4. Compile the list of changes |
| 51 | + |
| 52 | +**⚠️ Do NOT trust the previous version's git tag as the lower bound, and do NOT trust the new version's git tag (if it exists) as the upper bound.** Tags in this repo have historically been created out-of-order, prematurely, or left orphaned. The only reliable approach is to enumerate merged PRs by **merge date** on `main`. |
| 53 | + |
| 54 | +#### Step 4a. Establish the date range |
| 55 | + |
| 56 | +```powershell |
| 57 | +# Find the previous release's tag and its commit date |
| 58 | +git tag --sort=-v:refname | Select-Object -First 5 |
| 59 | +$prevTag = "v<PREV_VERSION>" # e.g. v0.2.1-alpha.0 — confirm with the team if uncertain |
| 60 | +$prevDate = git --no-pager log -1 --format="%cI" $prevTag |
| 61 | +Write-Host "Previous release tag: $prevTag at $prevDate" |
| 62 | +``` |
| 63 | + |
| 64 | +**Sanity check: is the tag stale?** If the tag's commit isn't on `main` HEAD or close to it, the tag may be premature/orphaned and shouldn't be used as an upper bound for the *next* release either: |
| 65 | + |
| 66 | +```powershell |
| 67 | +$commitsSinceTag = (git --no-pager log --oneline "$prevTag..main" | Measure-Object -Line).Lines |
| 68 | +Write-Host "$commitsSinceTag commits on main since $prevTag" |
| 69 | +# If this is suspiciously large (50+) and you weren't expecting that, investigate |
| 70 | +# before assuming the tag represents the last *real* release. |
| 71 | +``` |
| 72 | + |
| 73 | +If you find a tag that exists but has no matching `## [VERSION]` entry in `CHANGELOG.md` and no GitHub Release, treat it as **orphaned** — ignore it and use the most recent tag that *does* appear in `CHANGELOG.md` as your lower bound. |
| 74 | + |
| 75 | +#### Step 4b. Enumerate all merged PRs in the date range |
| 76 | + |
| 77 | +This is the authoritative list. **Do not** use `git log tag..tag` as a substitute — see "Why this matters" below. |
| 78 | + |
| 79 | +```powershell |
| 80 | +# Pull every PR merged on/after the previous release date. |
| 81 | +# Strip the time portion to be inclusive of release-day PRs. |
| 82 | +$sinceDate = ($prevDate -split "T")[0] |
| 83 | +gh pr list --state merged --base main --search "merged:>=$sinceDate" --limit 100 ` |
| 84 | + --json number,title,mergedAt,author ` |
| 85 | + --jq '.[] | "\(.mergedAt) #\(.number) [\(.author.login)] \(.title)"' ` |
| 86 | + | Sort-Object |
| 87 | +``` |
| 88 | + |
| 89 | +Cross-check against the commit log (sanity, not source of truth — `gh pr list` is canonical): |
| 90 | + |
| 91 | +```powershell |
| 92 | +git --no-pager log --merges --pretty=format:"%ci %s" "$prevTag..main" |
| 93 | +``` |
| 94 | + |
| 95 | +If the two lists disagree (e.g. a PR in `gh pr list` doesn't appear in `git log`), trust `gh pr list`. A PR can be missing from `git log tag..tag` because the tag was placed prematurely, the PR was rebased/squashed, or the PR landed on a branch that was later merged via another commit. |
| 96 | + |
| 97 | +#### Step 4c. Categorize the PRs |
| 98 | + |
| 99 | +Walk each PR and place it into one of these buckets. Use `gh pr view <N> --json title,body` to read details when the title alone isn't enough: |
| 100 | + |
| 101 | +- **Features** — new user-visible capabilities |
| 102 | +- **Bug Fixes** — corrections to existing behavior |
| 103 | +- **Docs & Testing** — documentation, examples, test improvements |
| 104 | +- **Breaking Changes** — anything that requires user action (add this section only if non-empty; call it out loudly even for pre-1.0) |
| 105 | +- **Skip from changelog** — internal repo tooling (issue templates, agentic workflows, label sync, squad/agent upgrades, CI hardening that doesn't affect end users). These don't belong in a user-facing changelog. If you're unsure, **ask the user** rather than guessing. |
| 106 | + |
| 107 | +#### Step 4d. Confirm the categorization with the user before writing the entry |
| 108 | + |
| 109 | +Show the user the categorized list and confirm: |
| 110 | + |
| 111 | +- Which PRs belong in the user-facing changelog vs. are internal-only |
| 112 | +- Whether any PR title is misleading and needs a clearer headline |
| 113 | +- Whether any change is breaking (especially CLI flag removals, schema changes, default-value changes) |
| 114 | + |
| 115 | +Only after this confirmation, proceed to draft the entry using the existing changelog style (short bolded headline → plain-language explanation → PR link): |
| 116 | + |
| 117 | +```markdown |
| 118 | +- **Token substitution in publish pipelines** — `{#[TOKEN_NAME]#}` placeholders are now resolved during publish, matching APIOps Toolkit behavior ([#127](https://github.com/Azure/apiops-cli/pull/127)) |
| 119 | +``` |
| 120 | + |
| 121 | +#### Why this matters (don't repeat past mistakes) |
| 122 | + |
| 123 | +A prior release attempt used `git log v<prev>..v<new>` to compile changes and found only 1 PR — when in reality **18 PRs** had merged in the range. Root cause: the `v<new>` tag had been placed on a commit 9 days before the actual release-bump PR merged, so `git log` only saw commits up to that stale tag. The user caught the mistake by eyeballing the GitHub PR list. |
| 124 | + |
| 125 | +**Lesson:** tags are not a reliable boundary in this repo. Always enumerate by `gh pr list --search "merged:>=<date>"` and cross-check against the screen the user sees on github.com/Azure/apiops-cli/pulls?q=is%3Amerged. |
| 126 | + |
| 127 | +### 5. Suggest a version and let the user pick |
| 128 | + |
| 129 | +Based on the compiled changes, suggest the next version following SemVer with the project's `-alpha.N` pre-release convention. Show the user a small menu via `ask_user` — always include an "other" option for a custom version. |
| 130 | + |
| 131 | +Decision rules (pre-1.0 era): |
| 132 | + |
| 133 | +- **Breaking change** → bump MINOR (`0.3.0-alpha.0` → `0.4.0-alpha.0`) |
| 134 | +- **New feature, no breaking change** → bump MINOR (`0.3.0-alpha.0` → `0.4.0-alpha.0`) |
| 135 | +- **Bug fix / docs only** → bump PATCH (`0.3.0-alpha.0` → `0.3.1-alpha.0`) |
| 136 | +- **Iterating on an unreleased pre-release** → bump the alpha number (`0.3.0-alpha.0` → `0.3.0-alpha.1`) |
| 137 | + |
| 138 | +Always validate the chosen version is real semver before continuing: |
| 139 | + |
| 140 | +```powershell |
| 141 | +node -e "const v=process.argv[1]; const s=require('semver'); if(!s.valid(v)){process.exit(1)}; console.log(v)" <NEW_VERSION> |
| 142 | +``` |
| 143 | + |
| 144 | +If it returns non-zero, STOP and ask the user for a different value. |
| 145 | + |
| 146 | +### 6. Update CHANGELOG.md |
| 147 | + |
| 148 | +Insert a new section directly under the `# Changelog` header (above the previous most-recent entry). Use ISO date, exactly match the version format used by the release workflow's grep (`## [VERSION] — YYYY-MM-DD`): |
| 149 | + |
| 150 | +```markdown |
| 151 | +## [0.3.1-alpha.0] — 2026-06-25 |
| 152 | + |
| 153 | +### Features |
| 154 | + |
| 155 | +- ... |
| 156 | + |
| 157 | +### Bug Fixes |
| 158 | + |
| 159 | +- ... |
| 160 | + |
| 161 | +### Docs & Testing |
| 162 | + |
| 163 | +- ... |
| 164 | +``` |
| 165 | + |
| 166 | +**Also add a compare-link footer at the bottom of the file.** CHANGELOG.md uses [Keep a Changelog](https://keepachangelog.com/) reference-style links so every `## [VERSION]` heading renders as a clickable link to the GitHub compare view between this and the previous release. The footer lives at the very bottom of the file and looks like: |
| 167 | + |
| 168 | +```markdown |
| 169 | +[0.3.1-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.3.0-alpha.0...v0.3.1-alpha.0 |
| 170 | +[0.3.0-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.2.1-alpha.0...v0.3.0-alpha.0 |
| 171 | +... |
| 172 | +``` |
| 173 | + |
| 174 | +Add a new line for the new version at the **top** of the footer block (most recent first), comparing against the **immediately preceding** version's tag. |
| 175 | + |
| 176 | +Verify both the workflow validator will accept the heading **and** the compare link is in place: |
| 177 | + |
| 178 | +```powershell |
| 179 | +Select-String -Path CHANGELOG.md -Pattern "^## \[<NEW_VERSION>\]" |
| 180 | +Select-String -Path CHANGELOG.md -Pattern "^\[<NEW_VERSION>\]:" |
| 181 | +``` |
| 182 | + |
| 183 | +Both `Select-String` calls must return a hit. If the first is empty the workflow will fail; if the second is empty the version heading won't render as a link in the published changelog (a pre-existing convention violation, not a workflow failure — but still wrong). |
| 184 | + |
| 185 | +### 7. Bump the version with `npm version` |
| 186 | + |
| 187 | +`npm version` updates `package.json` and `package-lock.json` atomically. By default it also creates a git tag and commit — **disable that** because the release workflow creates the tag for us once the PR merges. |
| 188 | + |
| 189 | +```powershell |
| 190 | +# Use --no-git-tag-version so we control the commit + tag separately |
| 191 | +# Cheat sheet (run ONE of these, not all): |
| 192 | +
|
| 193 | +# Pre-release patch: 0.3.0-alpha.0 -> 0.3.0-alpha.1 (increments pre-release identifier) |
| 194 | +npm version prerelease --preid=alpha --no-git-tag-version |
| 195 | +
|
| 196 | +# Patch: 0.3.0 -> 0.3.1 (only if you've dropped the pre-release suffix) |
| 197 | +npm version patch --no-git-tag-version |
| 198 | +
|
| 199 | +# Minor (pre-release): 0.3.0-alpha.0 -> 0.4.0-alpha.0 |
| 200 | +npm version preminor --preid=alpha --no-git-tag-version |
| 201 | +
|
| 202 | +# Major (pre-release): 0.3.0-alpha.0 -> 1.0.0-alpha.0 |
| 203 | +npm version premajor --preid=alpha --no-git-tag-version |
| 204 | +
|
| 205 | +# Explicit (recommended when you already chose the version in Step 5): |
| 206 | +npm version <NEW_VERSION> --no-git-tag-version |
| 207 | +``` |
| 208 | + |
| 209 | +Verify the result: |
| 210 | + |
| 211 | +```powershell |
| 212 | +node -p "require('./package.json').version" |
| 213 | +# Must equal <NEW_VERSION> |
| 214 | +``` |
| 215 | + |
| 216 | +### 8. Commit and open the pull request |
| 217 | + |
| 218 | +```powershell |
| 219 | +git add package.json package-lock.json CHANGELOG.md |
| 220 | +
|
| 221 | +# Use -F with a temp file so newlines render correctly (see CONTRIBUTING.md) |
| 222 | +$msg = @" |
| 223 | +chore: release v<NEW_VERSION> |
| 224 | +
|
| 225 | +Updates package.json and CHANGELOG.md for the v<NEW_VERSION> release. |
| 226 | +The squad-release workflow will create the git tag and GitHub Release |
| 227 | +once this PR merges to main. |
| 228 | +
|
| 229 | +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
| 230 | +"@ |
| 231 | +$msg | Out-File -Encoding utf8 commit-msg.txt |
| 232 | +git commit -F commit-msg.txt |
| 233 | +Remove-Item commit-msg.txt |
| 234 | +
|
| 235 | +git push -u origin HEAD |
| 236 | +``` |
| 237 | + |
| 238 | +Then open the PR. Use the `create_pull_request` tool (preferred — renders a card in the UI) with: |
| 239 | + |
| 240 | +- **Title:** `chore: release v<NEW_VERSION>` |
| 241 | +- **Body:** Paste the new CHANGELOG section verbatim, plus a line: "Once merged, `.github/workflows/squad-release.yml` will tag `v<NEW_VERSION>` and create the GitHub Release." |
| 242 | + |
| 243 | +### 9. After merge |
| 244 | + |
| 245 | +The squad-release workflow (`.github/workflows/squad-release.yml`) will: |
| 246 | + |
| 247 | +1. Validate the version is present in `CHANGELOG.md` |
| 248 | +2. Create the `v<NEW_VERSION>` git tag |
| 249 | +3. Create the matching GitHub Release |
| 250 | + |
| 251 | +No manual `git tag` or `gh release create` is needed. If the workflow fails, read the logs — the most common cause is a typo in the `## [VERSION]` header that fails the grep validator. |
| 252 | + |
| 253 | +## Pitfalls |
| 254 | + |
| 255 | +- **Don't run `npm version` without `--no-git-tag-version`.** It will create an unsigned tag on your release branch that conflicts with the one the workflow makes. |
| 256 | +- **Don't reformat unrelated changelog entries.** Limit the diff to the new section + the version bump. |
| 257 | +- **Don't squash-merge the release commit into something else.** It needs to land on `main` as a recognizable `chore: release v...` commit so reviewers can find it. |
| 258 | +- **Pre-1.0 breaking changes still deserve a callout.** Add a `### Breaking Changes` section even if SemVer technically allows the change in a MINOR bump. |
0 commit comments