mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-08-28 04:24:58 +00:00
feat(release): auto-tag + GitHub Release from CHANGELOG on push to main
Adds end-to-end release automation so every CHANGELOG bump produces a matching git tag + GitHub Release with notes — no manual `gh release create` invocations required. - .github/workflows/release.yml — push to main triggers parse-CHANGELOG → check tag exists → create tag → create GH Release with notes. Idempotent (existing tags skipped). Manual workflow_dispatch supports targeting a specific historical version. Path-filter limits firing to actual release pushes (CHANGELOG.md, the parser, or the workflow itself changing). - scripts/extract_release_notes.py — stdlib-only CHANGELOG.md parser. Outputs JSON, plain text, or github-release-formatted markdown. Runnable standalone for preview: `python3 scripts/extract_release_notes.py --format github-release`. Default extracts the latest entry; --version pins to a specific release. - CHANGELOG.md — new [2.8.0] entry covering the v2.8.0 Sprint 1 work (business-operations + commercial domains, #688) plus the #686 plugin.json fix (#689) and #690 regression-prevention validator + CI gate. This is the entry the release workflow will pick up on first run after this lands on main. When this commit (plus the dev → main sync #692) reaches main, the workflow fires, parses CHANGELOG, sees v2.8.0 at the top, creates the `v2.8.0` tag and GitHub Release — automatically. All future releases follow the same flow: add a CHANGELOG entry, merge to main, done.
This commit is contained in:
parent
f7bb1f86bc
commit
d34e615b73
3 changed files with 295 additions and 0 deletions
125
.github/workflows/release.yml
vendored
Normal file
125
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
# Auto-tag + GitHub Release from CHANGELOG.md.
|
||||
#
|
||||
# Trigger: every push to main. Parses CHANGELOG.md, takes the latest version
|
||||
# entry, and creates a git tag (v<version>) + GitHub Release with notes
|
||||
# extracted from that section. Idempotent — if the tag already exists, the
|
||||
# workflow skips with a notice. Manual re-runs via workflow_dispatch can
|
||||
# target a specific version.
|
||||
#
|
||||
# CHANGELOG.md header format expected:
|
||||
# ## [X.Y.Z] - YYYY-MM-DD — optional subtitle
|
||||
#
|
||||
# To cut a new release: add a new version entry to the top of CHANGELOG.md
|
||||
# and push to main. The workflow will fire on that push and create the tag.
|
||||
|
||||
name: Release
|
||||
|
||||
'on':
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
- '.github/workflows/release.yml'
|
||||
- 'scripts/extract_release_notes.py'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Specific version to release (e.g. 2.8.0). Default: latest in CHANGELOG.'
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write # needed to push tag + create release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Tag + GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # need full history to push tags
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Parse target version from CHANGELOG
|
||||
id: parse
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
python3 scripts/extract_release_notes.py --version "$VERSION" --format json > /tmp/release.json
|
||||
else
|
||||
python3 scripts/extract_release_notes.py --format json > /tmp/release.json
|
||||
VERSION=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['version'])")
|
||||
fi
|
||||
DATE=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['date'])")
|
||||
SUBTITLE=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['subtitle'])")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "date=$DATE" >> "$GITHUB_OUTPUT"
|
||||
echo "subtitle=$SUBTITLE" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Parsed: v$VERSION ($DATE) — $SUBTITLE"
|
||||
|
||||
- name: Check if tag already exists
|
||||
id: tagcheck
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.parse.outputs.tag }}"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Tag $TAG already exists locally."
|
||||
elif git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Tag $TAG already exists on remote."
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Tag $TAG does not exist — will create."
|
||||
fi
|
||||
|
||||
- name: Prepare release body
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
run: |
|
||||
python3 scripts/extract_release_notes.py \
|
||||
--version "${{ steps.parse.outputs.version }}" \
|
||||
--format github-release > /tmp/release-body.md
|
||||
echo "=== Release body preview ==="
|
||||
head -50 /tmp/release-body.md
|
||||
|
||||
- name: Create and push tag
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.parse.outputs.tag }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$TAG" -m "Release $TAG — ${{ steps.parse.outputs.subtitle }}"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.parse.outputs.tag }}"
|
||||
SUBTITLE="${{ steps.parse.outputs.subtitle }}"
|
||||
if [[ -n "$SUBTITLE" ]]; then
|
||||
TITLE="$TAG — $SUBTITLE"
|
||||
else
|
||||
TITLE="$TAG"
|
||||
fi
|
||||
gh release create "$TAG" \
|
||||
--title "$TITLE" \
|
||||
--notes-file /tmp/release-body.md \
|
||||
--verify-tag
|
||||
|
||||
- name: Skip (tag exists)
|
||||
if: steps.tagcheck.outputs.exists == 'true'
|
||||
run: |
|
||||
echo "::notice::Tag ${{ steps.parse.outputs.tag }} already exists. Skipping release creation."
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -5,6 +5,60 @@ All notable changes to the Claude Skills Library will be documented in this file
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.8.0] - 2026-05-19 — business-operations + commercial domains, plugin.json regression fix, auto-release pipeline
|
||||
|
||||
### Added
|
||||
|
||||
#### `business-operations/` — new top-level domain (Sprint 1)
|
||||
|
||||
Internal-ops skills for BizOps leads, COO direct reports, vendor management, IT ops. Sprint 1 ships the orchestrator + 2 sub-skills using `context: fork` to chain without polluting parent context. Sprint 2 will add `capacity-planner`, `internal-comms`, `knowledge-ops`, `procurement-optimizer`.
|
||||
|
||||
- `business-operations-skills` (orchestrator) — routes inquiries to the right sub-skill and returns a digest
|
||||
- `process-mapper` — BPMN modeling + bottleneck detection + cycle-time analysis
|
||||
- `vendor-management` — SLA tracking + risk scoring + supplier scorecards
|
||||
- Distinct from `business-growth/` (external sales) and `c-level-advisor/` (strategic, not operational)
|
||||
|
||||
#### `commercial/` — new top-level domain (Sprint 1)
|
||||
|
||||
Per-deal economics skills for pricing, deal desk, partnerships. Sprint 1 ships the orchestrator + 2 sub-skills. Sprint 2 will add `partnerships-architect`, `channel-economics`, `commercial-policy`, `rfp-responder`, `commercial-forecaster`.
|
||||
|
||||
- `commercial-skills` (orchestrator) — routes commercial inquiries via `context: fork`
|
||||
- `pricing-strategist` — Van Westendorp WTP analysis + packaging + pricing-model picker
|
||||
- `deal-desk` — margin analysis + discount routing + contract redline scoring
|
||||
- Distinct from `business-growth/sales-engineer`, `c-level-advisor/cro-advisor`, `finance/financial-analysis`
|
||||
|
||||
#### Forcing-question slash commands
|
||||
|
||||
- `/cs:grill-bizops` — Matt Pocock docs-anchored grilling for BizOps workflows
|
||||
- `/cs:grill-commercial` — same for commercial decisions (pricing, deals, partnerships)
|
||||
|
||||
#### Release automation
|
||||
|
||||
- **`.github/workflows/release.yml`** — On every push to `main`, parses CHANGELOG.md and auto-creates a git tag + GitHub Release for the latest version listed. Idempotent (skips if tag already exists). Release notes are extracted from the matching CHANGELOG section.
|
||||
- **`scripts/extract_release_notes.py`** — Stdlib-only CHANGELOG parser. Extracts the latest version, date, subtitle, and body. Used by the release workflow but also runnable standalone for previewing release notes.
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Plugin manifest `/doctor` warning — issue #686 (reported by @esoneill)
|
||||
|
||||
Claude Code 2.1.133+ rejects `"skills": "./skills"` with a "Path escapes plugin directory" warning, even though `./skills` resolves to a valid subdirectory inside the plugin root. This blocked skill registration for the 9 main marketplace plugins plus 38 sibling sub-plugins.
|
||||
|
||||
- **PR #689** — replaced `"skills": "./skills"` with `"skills": "skills"` across all 47 affected `plugin.json` files. Updated `CLAUDE.md` ClawHub publishing constraints to document the new convention.
|
||||
- **PR #690 (regression prevention)** — `scripts/check_plugin_json.py` now actively rejects any `"skills"` string starting with `"./"` (catches both `"./skills"` and `"./skills/sub"` regressions). Wired into `ci-quality-gate.yml` as a blocking step on every PR. Also recognized `source` and `attribution` as approved extension fields (per CLAUDE.md), and dropped the over-strict `"./"` rejection inside arrays (`["./"]` is the documented single-skill-at-root form). Validator's previous error message was actually recommending `"./skills"` verbatim — a leftover from #539, the *first* round of this same upstream rule tightening — which has been corrected.
|
||||
|
||||
This is the second round of the same Claude Code path-validator tightening (round 1 was #539, fixing `"./"` → `"./skills"` at CC v2.1.107). The new validator + CI gate prevents a future round 3 from silently shipping again.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- **inspect-assets.py** (#684, contributor: @TemaDeveloper) — `--help` now works without Pillow installed
|
||||
- Codex symlink syncs (automated)
|
||||
|
||||
### Stats
|
||||
|
||||
- 313 → 319 skills (business-operations: +3, commercial: +3)
|
||||
- 12 → 14 top-level domains
|
||||
- 60 → 68 slash commands
|
||||
|
||||
## [2.7.3] - 2026-05-17 — aeo-box port: AEO skill + security-guidance PreToolUse hook
|
||||
|
||||
### Added
|
||||
|
|
|
|||
116
scripts/extract_release_notes.py
Executable file
116
scripts/extract_release_notes.py
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Extract release notes for a specific version (or the latest) from CHANGELOG.md.
|
||||
|
||||
CHANGELOG.md must follow the Keep-a-Changelog header format:
|
||||
## [X.Y.Z] - YYYY-MM-DD - optional subtitle
|
||||
|
||||
Outputs JSON by default:
|
||||
{"version": "2.8.0", "date": "2026-05-19", "subtitle": "...", "body": "..."}
|
||||
|
||||
Or use --format=plain to print just the body, or --format=github-release to
|
||||
print a release body suitable for `gh release create --notes-file -`.
|
||||
|
||||
Examples:
|
||||
python3 scripts/extract_release_notes.py
|
||||
python3 scripts/extract_release_notes.py --version 2.7.3
|
||||
python3 scripts/extract_release_notes.py --format plain
|
||||
python3 scripts/extract_release_notes.py --format github-release
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CHANGELOG = REPO / "CHANGELOG.md"
|
||||
|
||||
# Matches: ## [2.8.0] - 2026-05-19 - optional subtitle text
|
||||
# Subtitle separator can be "-", "—", or absent.
|
||||
HEADER = re.compile(
|
||||
r"^##\s+\[(?P<version>\d+\.\d+\.\d+(?:-[\w.]+)?)\]\s*-\s*"
|
||||
r"(?P<date>\d{4}-\d{2}-\d{2})"
|
||||
r"(?:\s*[-—]\s*(?P<subtitle>.+))?\s*$"
|
||||
)
|
||||
|
||||
|
||||
def parse_changelog(text):
|
||||
"""Return list of {version, date, subtitle, body} dicts in file order."""
|
||||
lines = text.splitlines()
|
||||
entries = []
|
||||
current = None
|
||||
body_lines = []
|
||||
for line in lines:
|
||||
m = HEADER.match(line)
|
||||
if m:
|
||||
if current:
|
||||
current["body"] = "\n".join(body_lines).strip()
|
||||
entries.append(current)
|
||||
current = {
|
||||
"version": m.group("version"),
|
||||
"date": m.group("date"),
|
||||
"subtitle": (m.group("subtitle") or "").strip(),
|
||||
}
|
||||
body_lines = []
|
||||
elif current is not None:
|
||||
body_lines.append(line)
|
||||
if current:
|
||||
current["body"] = "\n".join(body_lines).strip()
|
||||
entries.append(current)
|
||||
return entries
|
||||
|
||||
|
||||
def select_entry(entries, version):
|
||||
if version is None:
|
||||
if not entries:
|
||||
return None
|
||||
return entries[0]
|
||||
for e in entries:
|
||||
if e["version"] == version:
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument("--version", help="Specific version to extract (default: latest)")
|
||||
ap.add_argument("--changelog", default=str(CHANGELOG), help="Path to CHANGELOG.md")
|
||||
ap.add_argument(
|
||||
"--format",
|
||||
choices=["json", "plain", "github-release"],
|
||||
default="json",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
path = Path(args.changelog)
|
||||
if not path.exists():
|
||||
print(f"CHANGELOG not found: {path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
entries = parse_changelog(path.read_text())
|
||||
entry = select_entry(entries, args.version)
|
||||
if not entry:
|
||||
if args.version:
|
||||
print(f"Version {args.version} not found in {path}", file=sys.stderr)
|
||||
else:
|
||||
print(f"No version entries found in {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.format == "json":
|
||||
print(json.dumps(entry, indent=2))
|
||||
elif args.format == "plain":
|
||||
print(entry["body"])
|
||||
elif args.format == "github-release":
|
||||
title_line = f"# {entry['version']} — {entry['subtitle']}" if entry["subtitle"] else f"# {entry['version']}"
|
||||
print(title_line)
|
||||
print(f"_Released {entry['date']}_")
|
||||
print()
|
||||
print(entry["body"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Reference in a new issue