claude-skills/.github/workflows/release.yml
Claude d34e615b73
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.
2026-05-19 04:00:06 +00:00

125 lines
4.5 KiB
YAML

---
# 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."