Release v2.1.0: Opus 4.8-tuned pm-ai-shipping audits + CHANGELOG-driven release automation

pm-ai-shipping: mandatory Evidence citations verified before reporting,
concrete subagent fan-out contract, read-only allowed-tools on both audits,
N+1/waterfall detection and a refute pass in the performance audit,
untrusted-input hardening across the kit, parallel audits in /ship-check,
severity anchors + report consolidation, repo-relative paths.

Repo: CHANGELOG.md as release source of truth with auto-tag-and-release on
merge to main (adapted from phuryn/claude-usage, minus the .vsix build),
Tests workflow on every PR/push, unit + docs-consistency test suite,
contributor-credit conventions in CONTRIBUTING, all manifests synced at 2.1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011URgT9hYuNrXeCvzjnqRxJ
This commit is contained in:
Claude
2026-07-03 13:34:34 +02:00
parent a0cd730d4c
commit 18468a95b4
27 changed files with 619 additions and 44 deletions
+174
View File
@@ -0,0 +1,174 @@
name: Tag and release from CHANGELOG
# Runs after each push to main. If CHANGELOG.md gained a new ## vX.Y.Z heading
# anywhere in this push's commit range (compared to the push's `before` SHA):
# 1. gate the release — the version in .claude-plugin/marketplace.json must
# match the new heading, and the validator + test suite must pass
# (the suite also asserts every plugin.json carries the same version), then
# 2. create a lightweight tag with that version name at the pushed commit, and
# 3. publish a GitHub Release for that tag with the matching CHANGELOG
# section as the notes.
#
# CHANGELOG is the source of truth; the tag and the Release are deterministic
# projections of it. Adapted from phuryn/claude-usage's tag-on-merge workflow,
# minus the .vsix build. Added headings whose tag already exists are treated as
# backfilled history and skipped, so importing old releases is safe.
#
# No action when CHANGELOG wasn't touched, when an existing version heading was
# edited (not added), or when the tag/release already exists. Safe to re-run on
# force-pushes and amends.
on:
push:
branches: [main]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
# Need enough history to diff the whole push range (`before..after`),
# not just the tip commit. Small repo; a full clone is cheap.
fetch-depth: 0
- name: Detect new version heading in CHANGELOG
id: detect
env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.sha }}
run: |
set -euo pipefail
# On a brand-new branch (first push), before is all zeros.
zeros="0000000000000000000000000000000000000000"
if [ "$BEFORE" = "$zeros" ] || [ -z "$BEFORE" ]; then
echo "version=" >> "$GITHUB_OUTPUT"
echo "Brand-new branch push; nothing to compare."
exit 0
fi
# Lines added to CHANGELOG.md across the entire pushed range that
# look like a version heading. Format: `## vX.Y.Z` (semver triplet
# required). The trailing-boundary group prevents `## v2.1.0a` from
# matching `v2.1.0`.
added_versions=$(git diff "$BEFORE..$AFTER" -- CHANGELOG.md \
| grep -E '^\+## v[0-9]+\.[0-9]+\.[0-9]+([[:space:]]|$)' \
| sed -E 's/^\+## (v[0-9]+\.[0-9]+\.[0-9]+)([[:space:]]|$).*/\1/' \
|| true)
if [ -z "$added_versions" ]; then
echo "version=" >> "$GITHUB_OUTPUT"
echo "No new ## vX.Y.Z heading added to CHANGELOG; nothing to tag."
exit 0
fi
# Headings whose tag already exists on origin are backfilled history,
# not new releases — skip them.
new_versions=""
for v in $added_versions; do
if git ls-remote --tags origin "refs/tags/$v" | grep -q .; then
echo "$v is already tagged; treating as backfill."
else
new_versions="${new_versions}${v}"$'\n'
fi
done
new_versions=$(printf '%s' "$new_versions" | sed '/^$/d')
if [ -z "$new_versions" ]; then
echo "version=" >> "$GITHUB_OUTPUT"
echo "All added headings are already tagged (backfill); nothing to do."
exit 0
fi
# If multiple new untagged headings were added in one push, fail
# loudly — ambiguous which one to tag, and shipping two releases in
# one merge is almost certainly not intended.
count=$(echo "$new_versions" | wc -l)
if [ "$count" -gt 1 ]; then
echo "::error::Multiple new untagged version headings detected; refusing to auto-tag. Versions: $new_versions"
exit 1
fi
version=$(echo "$new_versions" | head -1)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Detected new release: $version"
# ── Release gates ────────────────────────────────────────────────────
# Everything below is gated on a new version being detected, so ordinary
# pushes to main (docs, typo fixes) incur no setup or test cost.
- name: "Gate: marketplace.json version matches the CHANGELOG"
if: steps.detect.outputs.version != ''
env:
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -euo pipefail
want="${VERSION#v}"
have=$(python3 -c "import json; print(json.load(open('.claude-plugin/marketplace.json'))['version'])")
if [ "$have" != "$want" ]; then
echo "::error::marketplace.json is $have but CHANGELOG released $VERSION. Bump the manifests before the release push."
exit 1
fi
echo "marketplace.json at $have."
- name: "Gate: validator + test suite"
if: steps.detect.outputs.version != ''
run: |
set -euo pipefail
python3 validate_plugins.py
python3 -m unittest discover -s tests -v
- name: Create and push tag if it doesn't already exist
if: steps.detect.outputs.version != ''
env:
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -euo pipefail
# Tag may already exist if someone tagged manually before the
# workflow caught up, or on a re-push of the same commit. Idempotent.
if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q .; then
echo "Tag $VERSION already exists on origin; nothing to do."
exit 0
fi
git tag "$VERSION"
git push origin "$VERSION"
echo "Tagged $VERSION at $(git rev-parse HEAD)."
- name: Create GitHub Release with the CHANGELOG section as notes
if: steps.detect.outputs.version != ''
env:
VERSION: ${{ steps.detect.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Idempotent: a re-push of the same release commit shouldn't error.
if gh release view "$VERSION" >/dev/null 2>&1; then
echo "Release $VERSION already exists; nothing to do."
exit 0
fi
# Extract this version's CHANGELOG section (heading through the line
# before the next `## vX` heading) as the release notes. $2 is the
# version token: `## v2.1.0 — 2026-07-03` → $2 == "v2.1.0".
notes="$(mktemp)"
awk -v ver="$VERSION" '
/^## v[0-9]/ { if (started) exit; if ($2 == ver) started=1 }
started { print }
' CHANGELOG.md > "$notes"
if [ ! -s "$notes" ]; then
echo "::error::No '## $VERSION' section found in CHANGELOG.md."
exit 1
fi
gh release create "$VERSION" \
--title "$VERSION" \
--notes-file "$notes"
echo "Released $VERSION."
+28
View File
@@ -0,0 +1,28 @@
name: Tests
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.13"]
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Plugin validator
run: python validate_plugins.py
- name: Unit + consistency tests
run: python -m unittest discover -s tests -v