Skip to content

Commit cfc53ad

Browse files
copeusclaude
andcommitted
ci: auto-create a GitHub Release on version bump (close the releases-lag gap)
On push to main, if .claude-plugin/plugin.json's version has no matching tag yet, cut a GitHub Release (vX.Y.Z + CHANGELOG-extracted notes) via the built-in GITHUB_TOKEN (contents: write). Root cause of releases stuck at v2.1.1 while code shipped through v2.9.0: there was no release automation. Idempotent (skips if the tag exists); manual workflow_dispatch too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5c14411 commit cfc53ad

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Release on version bump
2+
3+
# When the plugin version in .claude-plugin/plugin.json is bumped on main and no matching
4+
# tag exists yet, cut a GitHub Release (tag vX.Y.Z + notes extracted from CHANGELOG.md).
5+
# This closes the gap that left releases stuck at v2.1.1 while the code shipped through v2.9.0.
6+
# Uses the built-in GITHUB_TOKEN (contents: write) — no user OAuth "workflow" scope needed.
7+
8+
on:
9+
push:
10+
branches: [main]
11+
paths:
12+
- ".claude-plugin/plugin.json"
13+
- "CHANGELOG.md"
14+
workflow_dispatch: {} # allow a manual re-run
15+
16+
permissions:
17+
contents: write
18+
19+
jobs:
20+
release:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0 # need tags + history
26+
27+
- name: Read plugin version
28+
id: ver
29+
run: |
30+
v=$(python3 -c "import json;print(json.load(open('.claude-plugin/plugin.json'))['version'])")
31+
echo "version=$v" >> "$GITHUB_OUTPUT"
32+
echo "Plugin version: $v"
33+
34+
- name: Skip if the tag already exists
35+
id: check
36+
run: |
37+
if git rev-parse -q --verify "refs/tags/v${{ steps.ver.outputs.version }}" >/dev/null \
38+
|| git ls-remote --exit-code --tags origin "v${{ steps.ver.outputs.version }}" >/dev/null 2>&1; then
39+
echo "exists=true" >> "$GITHUB_OUTPUT"
40+
echo "Tag v${{ steps.ver.outputs.version }} already exists — nothing to release."
41+
else
42+
echo "exists=false" >> "$GITHUB_OUTPUT"
43+
fi
44+
45+
- name: Extract CHANGELOG notes for this version
46+
if: steps.check.outputs.exists == 'false'
47+
id: notes
48+
run: |
49+
python3 - "${{ steps.ver.outputs.version }}" <<'PY'
50+
import re, sys, os
51+
v = sys.argv[1]
52+
cl = open("CHANGELOG.md", encoding="utf-8").read()
53+
parts = re.split(r'(?m)^## \[?\d+\.\d+\.\d+.*$', cl)
54+
heads = re.findall(r'(?m)^## \[?(\d+\.\d+\.\d+).*$', cl)
55+
body = ""
56+
for i, hv in enumerate(heads):
57+
if hv == v:
58+
body = parts[i + 1].strip()
59+
break
60+
if not body:
61+
body = "See the CHANGELOG for details."
62+
body = body[:6000].rstrip()
63+
body += "\n\nFull detail in the [CHANGELOG](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)."
64+
with open("_release_notes.md", "w", encoding="utf-8") as fh:
65+
fh.write(body)
66+
# a short human title line from the CHANGELOG heading, if any
67+
m = re.search(r'(?m)^## \[?%s\]?\s*(?:[—-]\s*(.*))?$' % re.escape(v), cl)
68+
title_suffix = (m.group(1).strip() if m and m.group(1) else "").strip()
69+
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
70+
fh.write("title_suffix=%s\n" % title_suffix)
71+
print("Extracted %d chars of notes." % len(body))
72+
PY
73+
74+
- name: Create the release
75+
if: steps.check.outputs.exists == 'false'
76+
env:
77+
GH_TOKEN: ${{ github.token }}
78+
V: ${{ steps.ver.outputs.version }}
79+
SUFFIX: ${{ steps.notes.outputs.title_suffix }}
80+
run: |
81+
title="v$V"
82+
if [ -n "$SUFFIX" ]; then title="v$V — $SUFFIX"; fi
83+
gh release create "v$V" --target "${{ github.sha }}" --latest \
84+
--title "$title" --notes-file _release_notes.md
85+
echo "Released v$V"

0 commit comments

Comments
 (0)