Release Notes Skill
Use this skill when the user wants to create release notes for a new version. This skill gathers all changes since a previous release, writes structured release notes following the project's established format, tags the repo, and pushes.
Input
The skill takes a version tag as input:
- Format:
{major}.{minor}.{patch}(e.g.,1.24.0) - novprefix, semver only - Older releases (pre-
1.23.0) used avprefix (e.g.,v1.0.22) - existing artifacts underdocs/release-notes/v*.mdand tagsv1.0.xare preserved as-is, but new releases must use the bare-semver convention - If the user provides a
v-prefixed version for a new release, strip the prefix and confirm
Output
Creates a release notes file in docs/release-notes/ and tags the repo:
docs/release-notes/{version}.md- Release notes markdown file (e.g.,docs/release-notes/1.24.0.md)- Git tag
{version}pointing to the commit that includes the release notes
Workflow
Step 0: Confirm the Pre-Release Smoke Test Was Run (Gate)
Before doing any release-notes work, confirm the end-to-end release smoke test
(tests/e2e_release_test.py) has been run
against a live gateway and passed. This suite exercises the surface a release
must not break: the registry is up, the built-in airegistry-tools server is
healthy and its search tool works, servers/agents/skills support full CRUD,
semantic search returns results, security scans run, and one real external MCP
server (AWS knowledge base) is reachable end to end through the gateway proxy.
Ask the user, using AskUserQuestion, whether they have already run the release smoke test and it passed. Offer these options:
- "Yes, it passed" (proceed to Step 1)
- "No, run it now" (Recommended - run it for them, see below)
- "Skip it" (proceed, but see the warning below)
If the user asks you to run it, run against the target gateway. It needs an admin/M2M bearer token file (Keycloak tokens expire in ~5 minutes, so regenerate first if unsure):
# Local gateway with an admin token at ./.token uv run python tests/e2e_release_test.py --token-file .token --registry-url http://localhost # Remote gateway uv run python tests/e2e_release_test.py \ --registry-url https://<gateway-host> --token-file .oauth-tokens/ingress.jsonThe runner prints a pass/fail table and exits non-zero if any test fails.
- If it exits non-zero (any FAILED), STOP. Do not proceed with the release. Report which tests failed and their messages, and help the user investigate. A release must not be cut with a failing smoke test.
- SKIPPED is acceptable (e.g. the external AWS test skips on a network outage rather than failing) - only FAILED is a hard block.
If the user chooses to skip it, warn once that the release is being cut without an end-to-end verification, then proceed only if they confirm.
Only after this gate is resolved do you continue to Step 1.
Step 1: Determine the New Version Tag
- Parse the version from user input. If not provided, ask the user what version to release.
- Normalize to bare semver format (e.g.,
v1.24.0becomes1.24.0). Never prependvfor new releases. - Verify the tag does not already exist:
git tag -l {version}. - If it exists, ask the user if they want to move it or choose a different version.
Step 2: Determine the Base Version (Ask User to Confirm)
The release notes are incremental from a previous version. Determine the base version:
- List existing release notes files (covers both old
v-prefixed and new bare-semver names):ls docs/release-notes/*.md - List existing git tags (any version-shaped tag, prefixed or bare):
git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' - Find the most recent tag. Note that the project switched from
v-prefixed (v1.0.22) to bare-semver (1.23.0,1.24.0) - the most recent bare-semver tag is the right base for a new release. - Ask the user to confirm the base version using AskUserQuestion. Present the most recent tag as the recommended option and the 2-3 previous tags as alternatives. The user may want to skip intermediate tags (e.g., diff from
1.23.0to1.25.0, skipping1.24.0).
Step 3: Gather All Changes Between Base and HEAD
Run these commands in parallel to gather change data:
# All commits (including merges) between base and HEAD
git log {base_tag}..HEAD --oneline
# Non-merge commits only (for detailed change analysis)
git log {base_tag}..HEAD --oneline --no-merges
# Merge commits (to extract PR numbers)
git log {base_tag}..HEAD --oneline --grep="Merge pull request"
# Contributors -- direct authors of commits on main
# WARNING: this misses co-authors of squash-merged PRs (Step 4 #10 explains).
git log {base_tag}..HEAD --format="%aN" | sort | uniq -c | sort -rn
# Contributors -- per-PR commit authors (catches squash-merge co-authors)
# Squash merges collapse N branch commits into 1 commit on main authored by
# the merger, so `git log` above will not show the actual code authors.
# `gh pr view --json commits` returns the original branch commits with their
# authors intact, which is the only reliable way to credit everyone.
for pr in $(git log {base_tag}..HEAD --oneline --grep="Merge pull request\|(#[0-9]\+)$" | grep -oE "#[0-9]+" | tr -d '#' | sort -u); do
gh pr view $pr --json number,author,commits \
--jq '"PR #\(.number) | opener: \(.author.login) | commit_authors: \([.commits[].authors[].name] | unique | join(", "))"' 2>/dev/null
done
# Env var changes
git diff {base_tag}..HEAD -- .env.example
# Helm chart changes (any file change inside charts/ requires
# `helm dependency build/update` for stack-chart consumers, even if
# Chart.yaml dependency lists are unchanged - subchart templates,
# values, and helpers are repackaged into .tgz on dependency rebuild).
git diff {base_tag}..HEAD -- charts/ --stat
# If ANY of these report changes, the upgrade instructions MUST tell
# Helm/EKS users to run `helm dependency build` and `helm dependency update`:
git diff {base_tag}..HEAD --stat -- 'charts/registry/' 'charts/auth-server/' 'charts/mcpgw/' 'charts/mcp-gateway-registry-stack/' 'charts/mongodb-configure/' 'charts/keycloak-configure/'
# Helm chart dependency-list changes (separate signal: added/removed deps)
git diff {base_tag}..HEAD -- charts/registry/Chart.yaml charts/auth-server/Chart.yaml charts/mcp-gateway-registry-stack/Chart.yaml charts/mcpgw/Chart.yaml
# Closed issues since the base tag was cut
# Use the base tag's date as the floor; gh issue list does not natively
# support "closed-since-tag", so we filter by closedAt timestamp.
BASE_TAG_DATE=$(git log -1 --format=%cI {base_tag})
gh issue list --state closed --limit 200 --json number,title,closedAt,labels \
--jq ".[] | select(.closedAt >= \"$BASE_TAG_DATE\") | \"\(.number) | \(.title) | \(.closedAt)\""
# Closed issues referenced by merged PRs in this release (most reliable mapping)
# For each PR number, the PR body usually has "Closes #N" or "Fixes #N" -- gh
# resolves these via the closingIssuesReferences field.
for pr in $(git log {base_tag}..HEAD --oneline --grep="Merge pull request" | grep -oE "#[0-9]+" | tr -d '#' | sort -u); do
gh pr view $pr --json number,title,closingIssuesReferences \
--jq '"\(.number) | \(.title) | closes: \(.closingIssuesReferences | map("#\(.number)") | join(","))"' 2>/dev/null
done
Step 4: Categorize Changes
Analyze all commits and PRs to categorize them:
Major Features: New capabilities that warrant their own section with description and PR link. Look for commits with
feat:prefix or PRs labeledenhancement/feature-request.Breaking Changes: Changes that require user action during upgrade. Check for:
- Helm chart dependency additions/removals (Chart.yaml changes)
- Renamed or removed environment variables (.env.example diff)
- Auth mechanism changes
- API endpoint changes (removed or renamed routes)
- Database schema changes
New Environment Variables: Extract from
.env.examplediff -- any new variables added.Bug Fixes: Commits with
fix:prefix or PRs labeledbug.Security Fixes: Commits mentioning security, CVE, injection, bypass, XSS, etc.
Infrastructure/Helm Changes: Changes to charts/, terraform/, docker/.
Dependency Updates: Dependabot PRs and manual dependency bumps.
Documentation: Commits with
docs:prefix.Closed Issues: Issues closed in the release window. Build from the
closingIssuesReferencesof every merged PR in this release (most reliable - GitHub auto-closes issues referenced byCloses #N/Fixes #Nin PR bodies), and supplement with manually-closed issues whoseclosedAtis between the base-tag commit date and HEAD. De-duplicate by issue number.Contributors: Build the union of TWO sources, because neither alone is complete:
- Direct authors on main:
git log {base_tag}..HEAD --format="%aN"-- catches anyone who pushed commits directly or whose PR was rebase/merge- committed. - Per-PR commit authors: for every merged PR in this release, run
gh pr view <num> --json commits --jq '[.commits[].authors[].name] | unique'and union the results -- catches co-authors of squash-merged PRs, whose branch commits get collapsed into a single commit on main authored by the merger. Without this step, every contributor on a squash-merged branch except the merger is silently dropped.
Then for each unique contributor name, resolve to a GitHub username by looking up a PR they appeared on:
- If they opened a PR:
gh pr view <num> --json author --jq .author.login. - If they were a co-author only (no PR opened):
gh pr view <num> --json commits --jq '.commits[].authors[] | select(.name == "<Display Name>") | .login'(the per-commitauthorsarray carries the GitHub login). - As a final fallback verify with
gh api users/<candidate>; a 404 means the guess is wrong. Never synthesize a username from a display name ("Amit Arora" -> "amitarora" was wrong; the actual login isaarora79).
- Direct authors on main:
Step 5: Write Release Notes
Create the file docs/release-notes/{version}.md following this exact structure
(note: bare semver, no v prefix, e.g. docs/release-notes/1.24.0.md):
# Release {version} - {Short Title Summarizing Major Features}
**{Month} {Year}**
---
## Upgrading from {base_version}
This section covers everything you need to know to upgrade from {base_version} to {version}.
### Breaking Changes
{List each breaking change with clear explanation and remediation steps.
If no breaking changes, write: "There are no breaking changes in this release."}
### New Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| {VAR_NAME} | {default} | {description} |
{If no new env vars, write: "No new environment variables in this release."}
### Upgrade Instructions
#### Docker Compose
```bash
cd mcp-gateway-registry
git pull origin main
git checkout {version}
# Review new env vars in .env.example and update your .env if needed
# Then rebuild and restart:
./build_and_run.sh
Kubernetes / Helm (EKS)
cd mcp-gateway-registry
git pull origin main
git checkout {version}
# {If helm dependency changes: "REQUIRED: Rebuild dependencies"}
cd charts/mcp-gateway-registry-stack
helm dependency build
helm dependency update
# Update values.yaml if needed, then upgrade:
helm upgrade mcp-gateway . -f your-values.yaml
{CRITICAL: Include the helm dependency build and helm dependency update
commands whenever ANY file under charts/ changes between base and HEAD,
not just Chart.yaml dependency-list changes. The packaged subchart .tgz
files inside charts/mcp-gateway-registry-stack/charts/ are gitignored
and only get repackaged when consumers run helm dependency build/update.
If subchart templates/values/helpers changed, a plain git pull followed
by helm upgrade will use the OLD packaged subcharts, missing the changes.
Only omit helm dependency build/update if git diff {base_tag}..HEAD --stat -- charts/
shows ZERO files changed.}
Terraform / ECS
cd mcp-gateway-registry
git pull origin main
git checkout {version}
# Update your .tfvars with any new variables
cd terraform/aws-ecs
terraform plan
terraform apply
Major Features
{Feature Name}
{Description of the feature -- what it does, why it matters, key capabilities as bullet points.}
{Repeat for each major feature.}
What's New
{Group changes by category using subsections. Use bullet points with PR/commit references.}
{Category Name}
- {Change description} (#{pr_number})
- {Change description} (#{pr_number})
{Common categories: Deployment, Helm Chart Improvements, Security Fixes, Authentication, Infrastructure, Frontend Improvements, Documentation. Only include categories that have changes.}
Bug Fixes
- {Bug fix description} (#{pr_number})
- {Bug fix description} (#{pr_number})
Closed Issues
| Issue | Title | Closed By |
|---|---|---|
| #{issue_number} | {issue_title} | {PR #{pr_number} or "manual"} |
{List all issues closed in the release window, sorted by issue number
descending. "Closed By" is the PR that closed the issue (via
closingIssuesReferences) or "manual" for issues closed without a PR
reference. If no issues were closed in this window, write:
"No issues were closed in this release window."}
Pull Requests Included
| PR | Title |
|---|---|
| #{number} | {title} |
{List ALL merged PRs between base and HEAD, sorted by PR number descending.}
Security Dependency Updates
| Package | Previous | Updated | Scope |
|---|---|---|---|
| {package} | {old_version} | {new_version} | {scope} |
{Only include this section if there are dependency version bumps.}
Contributors
Thank you to all contributors for this release:
- {Full Name} (@{github_username})
{List all contributors from the UNION of (a) direct authors on main and
(b) per-PR commit authors via gh pr view --json commits (Step 4 #10).
Resolve every GitHub username from a real PR -- via .author.login if they
opened a PR, or via .commits[].authors[].login if they were a co-author
only. Never synthesize usernames from display names. Sort by commit count
descending.}
Support
Full Changelog: {base_version}...{version}
### Step 6: Present Draft for User Review
After writing the release notes file:
1. Tell the user the file has been created at `docs/release-notes/{version}.md`
2. Present a brief summary:
- Number of major features
- Number of PRs included
- Number of bug fixes
- Number of closed issues
- Any breaking changes
- Contributor count
3. Ask the user to review the file and confirm it looks good, or request changes
### Step 7: Commit, Tag, and Push
Once the user confirms the release notes are ready:
1. **Confirm the new version appears in the `mkdocs.yml` Release Notes nav.** The nav uses a
single directory entry that auto-includes every file in `docs/release-notes/`:
```yaml
- Release Notes:
- release-notes
Because the whole directory is included automatically, a new docs/release-notes/{version}.md
file needs no mkdocs.yml edit — it is picked up on the next build. Verify with
mkdocs build (or check the built site/release-notes/{version}/ directory exists) and
confirm no new build warnings were introduced. Do NOT hand-maintain a per-version nav list;
the directory entry owns ordering. If the maintainer later wants an explicit descending
order, that is a separate, deliberate mkdocs.yml change — not part of the routine release cut.
Add a highlight entry and rotate the README's "What's New" (regrowth prevention). The README's
## What's Newsection holds exactly the 5 most-recent highlights; the full history lives indocs/overview/feature-release-highlights.md. On a release with a notable user-facing feature:- Prepend one curated highlight entry (headline + 1-3 sentences + doc links) to the top of
docs/overview/feature-release-highlights.md, just under its intro, matching the existing bullet format. - Rotate the README: add that same highlight as the new first bullet under
## What's NewinREADME.md, then delete the now-sixth bullet so exactly five remain (the dropped one already lives in the archive — this is a delete, not a re-copy). Keep the "Older highlights → Feature & Release Highlights" line in place. - This is the ONLY change the release cut makes to
README.md. Never add a new##section, never let What's New exceed 5 bullets, and never touch any other part of the README — those require their own dedicated PR. The README has a CI line-budget (350 lines) that will fail the build otherwise. A patch release with no user-facing feature skips this step entirely.
- Prepend one curated highlight entry (headline + 1-3 sentences + doc links) to the top of
Commit the release notes, highlights, README rotation, and nav together:
git add docs/release-notes/{version}.md docs/overview/feature-release-highlights.md README.md mkdocs.yml git commit -m "docs: Add {version} release notes"Push the commit:
git push origin mainCreate or move the git tag to point at this latest commit (which includes the release notes):
# If tag already exists, delete it locally and remotely first git tag -d {version} 2>/dev/null || true git push origin :refs/tags/{version} 2>/dev/null || true # Create tag on current HEAD (bare semver, no v prefix) git tag {version} # Push tag git push origin {version}Verify:
git log --oneline -1 git tag -l {version} --format="%(refname:short) -> %(objectname:short)"Tell the user the release notes are committed and the tag is created and pushed.
Important Rules
- Always run the Step 0 smoke-test gate first. Confirm the end-to-end release smoke test (
tests/e2e_release_test.py) was run and passed, or offer to run it. If it reports any FAILED test, STOP and do not cut the release. Only a user's explicit decision to skip may bypass this, and it must be warned about once. - Never skip the user confirmation for base version in Step 2. The user may want to create release notes that span multiple versions.
- Never include emojis in the release notes file. The project CLAUDE.md prohibits emojis in documentation.
- Never include Claude Code attribution or "Co-Authored-By" lines in commits.
- Always use the
docs/release-notes/directory for the output file. - Always include upgrade instructions for all three deployment methods (Docker Compose, Helm/EKS, Terraform/ECS).
- Always list breaking changes first in the upgrade section -- this is the most critical information for operators.
- Always verify Helm Chart.yaml diffs to detect dependency additions/removals -- these are the most common breaking changes for EKS users.
- Always check the full
charts/tree diff, not justChart.yaml. If ANY file undercharts/changed between base and HEAD, the upgrade instructions MUST includehelm dependency buildandhelm dependency updatefor stack-chart consumers. The packaged.tgzsubcharts insidecharts/mcp-gateway-registry-stack/charts/are gitignored and only repackage when those commands run -- a plaingit pull+helm upgradewill silently use stale subcharts. - Always credit squash-merge co-authors.
git log {base}..HEADonly sees the squashed commit's single author, so co-authors on the source branch get dropped. Always also iterate every merged PR withgh pr view <num> --json commits --jq '[.commits[].authors[].name] | unique'and union the results into the contributor list. - Never synthesize GitHub usernames from display names. Resolve every login from a real PR (
gh pr view <num> --json authorfor openers, or.commits[].authors[].loginfor co-authors), and verify uncertain ones withgh api users/<candidate>(404 = wrong guess). Past mistakes: "Amit Arora" ->amitarora(wrong; actual:aarora79); "Nathan Fernandes Pedroza" ->nathanfernandes(wrong; actual:nathanzilgo).
Example Usage
User: /release-notes v1.0.16