Skip to content

Commit b5a5221

Browse files
authored
Merge pull request #188 from Azure/chore/changelog-discoverability-and-release-skill
chore: link CHANGELOG from npm + README, add release skill
2 parents cf85a8e + 85cc497 commit b5a5221

4 files changed

Lines changed: 294 additions & 2 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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.

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,36 @@ All notable changes to the APIOps CLI are documented in this file.
55
The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66
This project uses [Semantic Versioning](https://semver.org/) with alpha pre-release tags.
77

8+
## [0.3.0-alpha.0] — 2026-06-25
9+
10+
### Breaking Changes
11+
12+
- **Removed unimplemented CLI options**`--spec-format` (extract command) and `--otel` (global) have been removed from the CLI, docs, and specs. Both were documented but never implemented, so no functional behavior changes for users who weren't relying on them ([#165](https://github.com/Azure/apiops-cli/pull/165))
13+
14+
### Features
15+
16+
- **Wildcard pattern matching in filter configuration** — all name-based filter fields (`apis`, `backends`, `namedValues`, etc.) now accept glob-style patterns with `*` and `?`. Exact names still work alongside patterns ([#160](https://github.com/Azure/apiops-cli/pull/160))
17+
- **Workspace sub-resource filtering** — nested workspace filters are now applied at runtime (previously documented but not implemented). Wildcard patterns in the `workspaces` list trigger discovery-then-filter instead of being used as literal names. Adds `schemas` field on `WorkspaceSubFilter` ([#160](https://github.com/Azure/apiops-cli/pull/160))
18+
- **Token substitution validation in generated GitHub Actions publish workflow**`apiops init` now adds a validation step between token substitution and publish that greps for unresolved \`{#[...]#}\` tokens and fails the job with a clear list of unmapped names. Mirrors the existing Azure DevOps behavior ([#158](https://github.com/Azure/apiops-cli/pull/158))
19+
- **MCP publish from embedded API metadata** — MCP configuration is now read/written exclusively from `apiInformation.json`; the redundant `mcpServerInformation.json` sidecar is no longer produced or consumed. Legacy sidecars are ignored. Tool operation references are rewritten from source to target ARM IDs before override application so overrides still win ([#173](https://github.com/Azure/apiops-cli/pull/173))
20+
- **Association scope tracking for round-trip correctness** — extract/publish now records and reconstructs whether each linked resource (e.g. workspace product → built-in group) is service-scoped or workspace-scoped, producing correct ARM paths in both directions. Backward-compatible with legacy artifact files that only contain `name`. 409 "link already exists" responses are treated as idempotent successes ([#175](https://github.com/Azure/apiops-cli/pull/175))
21+
22+
### Bug Fixes
23+
24+
- **Resource group log masking preserves meaningful segments**`Protect-ResourceGroupName` now detects resource group names ending with a long numeric suffix (>=6 digits, typical of GitHub `run_id` values) and preserves the last dash-separated segment before the number. Output goes from `rg-...1553476` to `rg-...src-27781553476`, making source vs target groups distinguishable in logs ([#168](https://github.com/Azure/apiops-cli/pull/168))
25+
- **Override config template no longer hardcodes a secret** — the generated `configuration.override.yaml` from `apiops init` now uses a \`{#[DB\_Connection\_String]#}\` token placeholder for the connection-string example (aligned with the pipeline substitution format) and drops the confusing "APIOps Toolkit" reference comment ([#164](https://github.com/Azure/apiops-cli/pull/164))
26+
27+
### Docs & Testing
28+
29+
- **Incremental publish behavior documented** — clarified that changes to override files alone don't trigger incremental publish; only API/policy/etc. artifact changes do ([#159](https://github.com/Azure/apiops-cli/pull/159))
30+
- **Environment overrides guide improvements** — expanded examples and clarifications in `docs/guides/environment-overrides.md` ([#166](https://github.com/Azure/apiops-cli/pull/166))
31+
- **All-types round-trip test imports Petstore Swagger** — the all-resource-types integration test now imports the canonical Petstore Swagger spec in both V2 and V3 forms, exercising a broader surface ([#171](https://github.com/Azure/apiops-cli/pull/171))
32+
833
## [0.2.1-alpha.0] — 2026-06-12
934

1035
### Features
1136

12-
- **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))
37+
- **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))
1338

1439
### Bug Fixes
1540

@@ -113,6 +138,7 @@ This project uses [Semantic Versioning](https://semver.org/) with alpha pre-rele
113138
- **Initial release** — core extract, publish, and init commands for Azure API Management ([#15](https://github.com/Azure/apiops-cli/pull/15))
114139
- **CodeQL analysis** — automated security scanning workflow ([#19](https://github.com/Azure/apiops-cli/pull/19))
115140

141+
[0.3.0-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.2.1-alpha.0...v0.3.0-alpha.0
116142
[0.2.1-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.2.0-alpha.0...v0.2.1-alpha.0
117143
[0.2.0-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.1.7-alpha.0...v0.2.0-alpha.0
118144
[0.1.7-alpha.0]: https://github.com/Azure/apiops-cli/compare/v0.1.6-alpha.0...v0.1.7-alpha.0

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ apiops init \
141141
| `--client-secret <secret>` | `AZURE_CLIENT_SECRET` env var | Service principal secret |
142142
| `--tenant-id <id>` | `AZURE_TENANT_ID` env var | Azure AD tenant ID |
143143

144+
## Changelog
145+
146+
See [CHANGELOG.md](./CHANGELOG.md) for release notes and the list of changes in each version. To see what has changed
147+
since the version you have installed, compare tags on GitHub
148+
(e.g. [`v0.2.1-alpha.0...main`](https://github.com/Azure/apiops-cli/compare/v0.2.1-alpha.0...main)).
149+
144150
## Contributing
145151

146152
See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to build, test, debug, and submit changes.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"apiops": "dist/cli/index.js"
1111
},
1212
"files": [
13-
"dist"
13+
"dist",
14+
"CHANGELOG.md"
1415
],
1516
"readme": "README.md",
1617
"scripts": {
@@ -44,6 +45,7 @@
4445
"url": "https://github.com/Azure/apiops-cli/issues"
4546
},
4647
"homepage": "https://github.com/Azure/apiops-cli#readme",
48+
"changelog": "https://github.com/Azure/apiops-cli/blob/main/CHANGELOG.md",
4749
"dependencies": {
4850
"@azure/identity": "^4.13.1",
4951
"any-ascii": "^0.3.3",

0 commit comments

Comments
 (0)