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 One & Headline Theme Two (not plain vX.Y.Z). Same theme(s) you'd put in a tweet.## 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:
This project's CLAUDE.md treats response-shape changes as breaking → major bump. Honor that. But the user impact in MCP context is smaller than in SDK context:
response["data"]["field"]) are rare in MCP usage.So when a release breaks response shape, the release-notes treatment should be 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.Print the contents of the temp file. The user may want to tweak before publishing. Wait for explicit go-ahead before step 4.
# 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. 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.