Skip to content

release: v3.5.1 — contributed V-memory embedding sub-batches (#9, @kh… #54

release: v3.5.1 — contributed V-memory embedding sub-batches (#9, @kh…

release: v3.5.1 — contributed V-memory embedding sub-batches (#9, @kh… #54

Workflow file for this run

name: Release on version bump
# When the plugin version in .claude-plugin/plugin.json is bumped on main and no matching
# tag exists yet, cut a GitHub Release (tag vX.Y.Z + notes extracted from CHANGELOG.md).
# This closes the gap that left releases stuck at v2.1.1 while the code shipped through v2.9.0.
# Uses the built-in GITHUB_TOKEN (contents: write) — no user OAuth "workflow" scope needed.
on:
push:
branches: [main]
paths:
- ".claude-plugin/plugin.json"
- "CHANGELOG.md"
workflow_dispatch: {} # allow a manual re-run
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need tags + history
- name: Read plugin version
id: ver
run: |
v=$(python3 -c "import json;print(json.load(open('.claude-plugin/plugin.json'))['version'])")
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Plugin version: $v"
# Gate on the RELEASE, not the tag. Gating on the tag made this job a silent
# no-op for any maintainer who tags by hand before CI runs — which is exactly
# what happened to v3.0.4 and v3.0.5: both were tagged, both went green, and
# neither was ever published. The skip logged "nothing to release" and looked
# correct. `gh release create` accepts an existing tag, so a hand-pushed tag is
# no longer a reason to skip.
- name: Skip if the release already exists
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "v${{ steps.ver.outputs.version }}" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Release v${{ steps.ver.outputs.version }} already exists — nothing to release."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Extract CHANGELOG notes for this version
if: steps.check.outputs.exists == 'false'
id: notes
run: |
python3 - "${{ steps.ver.outputs.version }}" <<'PY'
import re, sys, os
v = sys.argv[1]
cl = open("CHANGELOG.md", encoding="utf-8").read()
parts = re.split(r'(?m)^## \[?\d+\.\d+\.\d+.*$', cl)
heads = re.findall(r'(?m)^## \[?(\d+\.\d+\.\d+).*$', cl)
body = ""
for i, hv in enumerate(heads):
if hv == v:
body = parts[i + 1].strip()
break
if not body:
body = "See the CHANGELOG for details."
body = body[:6000].rstrip()
body += "\n\nFull detail in the [CHANGELOG](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)."
with open("_release_notes.md", "w", encoding="utf-8") as fh:
fh.write(body)
# A short human title. The CHANGELOG heading is `## [X.Y.Z] - YYYY-MM-DD`, so
# what follows the dash is the DATE, not a name — v3.0.6 and v3.1.0 both shipped
# titled "vX.Y.Z — 2026-09-02" before this was noticed. Take the first `### `
# heading of the body instead, which is where the release actually names itself,
# and fall back to no suffix rather than to a date.
m = re.search(r'(?m)^## \[?%s\]?\s*(?:[—-]\s*(.*))?$' % re.escape(v), cl)
title_suffix = (m.group(1).strip() if m and m.group(1) else "").strip()
if re.fullmatch(r'\d{4}-\d{2}-\d{2}', title_suffix or ""):
title_suffix = ""
if not title_suffix:
h = re.search(r'(?m)^###\s+(.+?)\s*$', body)
if h:
# "Fixed — a two-line change no longer runs twenty thousand tests"
# reads better as the half after the dash.
t = h.group(1).strip()
parts = re.split(r'\s+[—-]\s+', t, maxsplit=1)
title_suffix = (parts[1] if len(parts) > 1 else t).strip()[:80]
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
fh.write("title_suffix=%s\n" % title_suffix)
print("Extracted %d chars of notes." % len(body))
PY
# A release must not publish on a red validate. Both workflows start on the
# same push; on 2026-09-03 v3.4.0 was published while "Validate Plugin" was
# failing on the very same commit (the anti-ruflo gate), and nothing connected
# the two. Wait, bounded, for that run on THIS sha and require success.
- name: Require "Validate Plugin" green on this commit
if: steps.check.outputs.exists == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
for i in $(seq 1 40); do
line=$(gh run list --workflow validate.yml --commit "${{ github.sha }}" --json status,conclusion --jq '.[0] | "\(.status) \(.conclusion // "")"' 2>/dev/null || true)
case "$line" in
"completed success") echo "Validate Plugin: success"; exit 0 ;;
completed*) echo "❌ Validate Plugin concluded '$line' on ${{ github.sha }} — not releasing"; exit 1 ;;
*) echo "waiting for Validate Plugin ($line)…"; sleep 30 ;;
esac
done
echo "❌ Validate Plugin did not finish in time — not releasing"; exit 1
- name: Create the release
if: steps.check.outputs.exists == 'false'
env:
GH_TOKEN: ${{ github.token }}
V: ${{ steps.ver.outputs.version }}
SUFFIX: ${{ steps.notes.outputs.title_suffix }}
run: |
title="v$V"
if [ -n "$SUFFIX" ]; then title="v$V — $SUFFIX"; fi
# --target is honoured only when the tag does not exist yet; an existing
# hand-pushed tag keeps whatever commit it already points at, which is the
# correct behaviour — the tag is the maintainer's statement of what shipped.
gh release create "v$V" --target "${{ github.sha }}" --latest \
--title "$title" --notes-file _release_notes.md
echo "Released v$V"