mirror of
https://github.com/phuryn/pm-skills.git
synced 2026-07-14 12:15:17 +03:00
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:
@@ -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."
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user