release-write — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited release-write (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This repo's release path is half-manual:
| Step | Trigger | Workflow |
|---|---|---|
Push chore(release): X.Y.Z to main | push.branches: main | release.yml → Docker build/push |
Push tag vX.Y.Z | (none) | nothing |
| Create GitHub Release on the tag | release.types: [published] | publish.yml → PyPI + MCP Registry |
Creating the GitHub Release is what actually triggers PyPI publish. It is not optional. CHANGELOG.md is manual (per CLAUDE.md). This skill writes the GitHub Release body so users opening the Releases tab or the PyPI project page see polished notes, not raw commit history.
chore(release): X.Y.Z) is already on origin/main — i.e. CHANGELOG.md has been renamed from [Unreleased] to [X.Y.Z] — YYYY-MM-DD, committed, and pushed.make can-release is green.main, up to date with origin/main.expected length <= 100), failing the mcp-registry job after PyPI has already published — a partial release that's annoying to recover. Pre-flight check: jq -r '.description | length' server.json. If it's over 100, stop and get it trimmed on main first.If any prereq is missing, stop and tell the user what to do first.
Find the start line of ## [X.Y.Z] and the start line of the next version section below it. Read everything in between, dropping the leading ## [...] — date heading line (GitHub uses the release title instead).
# Example for v3.0.0:
awk '/^## \[3\.0\.0\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.mdSave the extracted body to a temp file (e.g. /tmp/release-notes-X.Y.Z.md).
Critical: the release notes are NOT a copy of the CHANGELOG section. The CHANGELOG is the exhaustive source-of-truth; the release body is a curated headline version. Past releases in this repo (v2.0.0, v1.3.0, v1.2.0) follow a narrative style — read 1-2 of them before drafting to anchor on the existing voice:
gh release view vPREVIOUS --json body --jq .bodyAdopt these conventions:
vX.Y.Z – Headline Theme (not plain vX.Y.Z). Same theme you'd put in a tweet.Theme One & Theme Two) is allowed only when both are independently headline-worthy — the shape of v2.0.0 and v4.4.0. Don't reach for a second half to fill out the line; the best titles here are the single-theme ones (v4.3.2, v1.2.0). Aim for ≤ 45 chars including the vX.Y.Z – prefix.## Upgrading from vX.Y section, not the title — even on a major.&. No trailing appositives or comma clauses (… & The Deferred Cleanups, Drained is the failure case).## New features, ## Improvements, ## Notes, or headline-named sections like ## Delete Safety Mode. DO NOT use `### Added / ### Fixed / ### Changed` — that's CHANGELOG style; the release notes are different.## Upgrading from vX.Y heading with prose paragraphs (NOT diff snippets unless the consumer audience is parsing-heavy — see below). **Full Changelog:** https://github.com/<owner>/<repo>/compare/v<PREVIOUS>...v<CURRENT>This replaces a hand-curated PR list. Don't list PRs unless the user explicitly asks for it.
MCP-specific framing for response-shape changes:
Per the narrowed SemVer contract (CHANGELOG header, adopted after 4.0.0), information-preserving response-shape changes (key renames, restructuring, added fields) are minor, not breaking. Only tool/parameter removals or renames, config env var semantic changes, default-registration changes, and removal of information from responses force a major. The reasoning:
response["data"]["field"]) are rare in MCP usage.When a release does include a breaking change — or a notable shape change worth flagging — keep the release-notes treatment proportional to the actual user impact: one paragraph of inline upgrade prose, not an SDK-style Migration section with before/after diff blocks. Diff blocks are for projects where consumers write typed parsing code; they're overkill here.
Don't:
### Added, etc.) — that's the wrong genre.Nobody reads long release notes. The first draft is always too long, because every fact feels load-bearing while you're holding the whole diff in your head. It isn't: the CHANGELOG is one click away and holds the evidence. Run this pass every time, even when the draft already feels tight.
Word budget (body only, excluding the Full Changelog line) — measure it, don't estimate:
wc -w < /tmp/release-notes-X.Y.Z.md| Release | Target | Hard ceiling |
|---|---|---|
| Patch (X.Y.Z) | ≤ 120 | 150 |
| Minor (X.Y.0) | ≤ 250 | 300 |
| Major (X.0.0, incl. upgrade section) | ≤ 350 | 400 |
Historical anchors: v2.0.0 = 192 words, v3.0.0 = 415, v4.0.0 = 182. A major that lands at 800 has failed this step. Per section: one theme, ≤ 80 words. More than four ## sections means you're listing, not curating — merge or drop.
Over budget? Cut in this order (each of these is CHANGELOG material, not release-notes material):
Keep, always: what broke, what it means for the reader's own data, the exact tool/field names to change, and the contributor credits.
Then re-read the trimmed draft and ask: does it still say the same thing? If a cut lost information a user actually needs to act, restore that sentence — and take the words back out somewhere else, not from the budget.
Print the contents of the temp file and its word count against the budget (e.g. "312 words, major budget 350"). The user may want to tweak before publishing. Wait for explicit go-ahead before step 5.
# Create the tag (no automation; just a marker)
git tag vX.Y.Z
git push origin vX.Y.Z
# Create the GitHub Release using the polished notes file
# This is what triggers publish.yml → PyPI + MCP Registry
gh release create vX.Y.Z --title "vX.Y.Z – <tagline>" --notes-file /tmp/release-notes-X.Y.Z.mdDo NOT use --generate-notes — that uses GitHub's auto-generated PR/commit list, which is noisier than what we just polished.
Do NOT use --draft. The release needs to be published to trigger publish.yml.
gh release view vX.Y.Z --json url,tagName,publishedAtReport the URL to the user. Then watch the publish workflow to completion:
gh run watch "$(gh run list --workflow=publish.yml --limit=1 --json databaseId --jq '.[0].databaseId')" --exit-statuspublish.yml has two jobs: pypi then mcp-registry (needs: pypi). Watch both — the registry job can fail after PyPI has published, leaving a partial release (PyPI live, registry missing). Don't report success off the PyPI job alone.
If it fails, surface the failure (gh run view <id> --log-failed) and stop — do not retry without user confirmation. Recovery notes:
main, force-move the tag, and delete+recreate the Release to re-fire the workflow (the pypi job has skip-existing: true, so it no-ops on the already-published version), or (b) publish to the registry manually: download mcp-publisher from modelcontextprotocol/registry, jq the release version into server.json, ./mcp-publisher login github (interactive device flow), ./mcp-publisher publish.curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=<pkg>" | jq '.servers[]?.version'.publish.yml only fires on release.types: [published], not on draft creation.The release body should answer three questions for a reader scanning the Releases tab:
If a reader can answer those without clicking into the CHANGELOG, the polish is sufficient — and anything beyond those three answers is a candidate for the cut pass. Avoid duplicating the CHANGELOG into a verbose PR list at the bottom; the comparison link is enough.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.