release: v3.5.1 — contributed V-memory embedding sub-batches (#9, @kh… #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |