From 0c4335bfc975020939f975a22905fc83b6164031 Mon Sep 17 00:00:00 2001 From: Anton Babenko Date: Sat, 16 May 2026 23:50:33 +0200 Subject: [PATCH] feat: path-based per-plugin release detection Drop the scope-required release gate. A commit now qualifies for a plugin if it changed release-worthy content under plugins// (anything except tests/ and the CI-managed CHANGELOG.md) OR its subject is scoped to the plugin (back-compat). The conventional-commit type sets the bump (feat -> minor, fix/perf/refactor -> patch, !/BREAKING -> major); a plugin with changes but no typed commit is not released. Loop-safe: bot 'chore(release)' commits are type chore (no bump) and the release commit is excluded by the plugin's own tag range. Shared .claude-plugin/marketplace.json is outside plugins// so it never counts as a plugin path. - automated-release.yml: add touches_plugin(); qualify = path OR scope; header comment updated - CLAUDE.md / CONTRIBUTING.md: release tables rewritten to the path+type model This change touches only .github + docs (no plugins/

content) so it triggers no release itself; once on master the existing in-range fc383cc (PR #2, feat:, code-intelligence content) releases code-intelligence 0.2.0 -> 0.3.0 as intended. --- .github/workflows/automated-release.yml | 43 ++++++++++++++++++++----- CLAUDE.md | 31 +++++++++++------- CONTRIBUTING.md | 31 ++++++++++-------- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/.github/workflows/automated-release.yml b/.github/workflows/automated-release.yml index 2e95736..8ac69b2 100644 --- a/.github/workflows/automated-release.yml +++ b/.github/workflows/automated-release.yml @@ -3,13 +3,17 @@ name: Automated Release # Per-plugin release pipeline. # # Each plugin in .claude-plugin/marketplace.json is versioned and tagged -# independently. A push to master is analyzed per plugin: only conventional -# commits scoped to a plugin (e.g. `feat(terraform-skill): ...`) bump that -# plugin. Unscoped commits never trigger a release. +# independently. A push to master is analyzed per plugin. A commit +# qualifies for a plugin if it changed release-worthy content under +# plugins// (anything except tests/ and the CI-managed +# CHANGELOG.md) OR its subject is explicitly scoped to the plugin +# (back-compat). The conventional-commit TYPE of qualifying commits sets +# the bump; a plugin with changes but no typed commit is not released. # -# feat()! / BREAKING CHANGE -> major -# feat() -> minor -# fix|perf|refactor() -> patch +# feat! / BREAKING CHANGE -> major +# feat -> minor +# fix | perf | refactor -> patch +# chore/docs/ci/test/... -> no bump (loop-safe for bot release commits) # # For each bumped plugin the workflow: # - bumps plugins[].version in marketplace.json @@ -108,13 +112,35 @@ jobs: scope_re = re.compile( r'^(?P\w+)(?:\((?P[^)]+)\))?(?P!)?:\s*(?P.+)$') + # A commit qualifies for this plugin if it changed + # release-worthy content under plugins// (anything + # except tests/ and the CI-managed CHANGELOG.md) OR its + # subject is explicitly scoped to the plugin (back-compat). + # The conventional-commit TYPE then sets the bump. + def touches_plugin(files, src): + pref = src.rstrip('/') + '/' + for f in files: + if not f.startswith(pref): + continue + rel = f[len(pref):] + if rel == 'CHANGELOG.md' or rel.startswith('tests/'): + continue + return True + return False + for rec in commits: parts = rec.strip('\n').split('\x1f') if len(parts) < 3: continue chash, subject, body = parts[0], parts[1], parts[2] m = scope_re.match(subject.strip()) - if not m or m.group('scope') != name: + if not m: + continue + files = [p for p in run( + 'git', 'diff-tree', '--no-commit-id', '--name-only', + '-r', chash).splitlines() if p.strip()] + if not (m.group('scope') == name + or touches_plugin(files, source)): continue short = chash[:7] desc = m.group('desc').strip() @@ -127,7 +153,8 @@ jobs: fixes.append((short, desc)) if not (feats or fixes or breaking): - print(f"SKIP {name}: no scoped commits since {last_tag or 'start'}") + print(f"SKIP {name}: no qualifying typed commits since " + f"{last_tag or 'start'}") continue major, minor, patch = semver(cur) diff --git a/CLAUDE.md b/CLAUDE.md index d1ad42d..e9865e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,18 +136,25 @@ No automated suite. Manual flow: Releases are **fully automated and per-plugin, for inline plugins only**. External plugins release in their own repos; you ship a newer one here by bumping its `source.ref`. The release pipeline analyzes each push to `master` -and bumps each inline plugin independently from **plugin-scoped** conventional -commits. - -| Commit | Effect | -|--------|--------| -| `feat()!:` or body `BREAKING CHANGE:` | Major bump for `` | -| `feat(): ...` | Minor bump for `` | -| `fix(): ...`, `perf():`, `refactor():` | Patch bump | -| Commit with no plugin scope (or unknown scope) | No release | - -**The commit scope must equal the plugin name.** A commit scoped to -`terraform-skill` never moves any other plugin. Per release the workflow: +and bumps each inline plugin independently. + +**A commit qualifies for a plugin** if it changed release-worthy content +under `plugins//` - anything except `tests/` and the CI-managed +`CHANGELOG.md` - **OR** its subject is explicitly scoped to the plugin +(`feat(): ...`, back-compat). The conventional-commit **type** of the +qualifying commits then sets the bump: + +| Qualifying commit type | Effect | +|------------------------|--------| +| `feat!:` / `feat()!:` / body `BREAKING CHANGE:` | Major bump | +| `feat: ...` (or scoped) | Minor bump | +| `fix: ...`, `perf:`, `refactor:` (or scoped) | Patch bump | +| `chore`/`docs`/`ci`/`test`/`style`, or no conventional type | No bump | +| Change touches only `tests/`, `CHANGELOG.md`, or no plugin content | No release | + +A squash commit touching two plugins bumps both (each from that commit's +type). Bot release commits (`chore(release): ...`) never bump - type `chore` +- so the pipeline is loop-safe. Per release the workflow: - bumps `plugins[].version` in `marketplace.json`, - syncs that plugin's `SKILL.md` `metadata.version`, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f824031..2a69ff4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,19 +59,24 @@ content shape and token discipline. ## Commits & Releases -Releases are automated and **per-plugin, for inline plugins only**, driven by -the commit scope on `master`. The scope must equal the plugin name. External -plugins release upstream; update them here by bumping `source.ref`. - -| Commit subject | Result | -|----------------|--------| -| `feat(): ...` | minor bump for `` | -| `fix(): ...` / `perf` / `refactor` | patch bump | -| `feat()!: ...` or `BREAKING CHANGE:` in body | major bump | -| no plugin scope | no release | - -A commit scoped to one plugin never affects another. PRs are squash-merged, so -the squash commit subject is what drives the release; set it deliberately. +Releases are automated and **per-plugin, for inline plugins only**. A commit +qualifies for a plugin when it changes release-worthy content under +`plugins//` (anything except `tests/` and the CI-managed +`CHANGELOG.md`), or when its subject is scoped to that plugin +(`feat(): ...`, back-compat). The commit **type** sets the bump. +External plugins release upstream; update them here by bumping `source.ref`. + +| Qualifying commit type | Result | +|------------------------|--------| +| `feat: ...` (or scoped) | minor bump | +| `fix:` / `perf:` / `refactor:` (or scoped) | patch bump | +| `feat!:` or `BREAKING CHANGE:` in body | major bump | +| `chore`/`docs`/`ci`/`test`, or no conventional type | no release | +| touches only `tests/`, `CHANGELOG.md`, or no plugin content | no release | + +PRs are squash-merged, so the squash subject's type plus the changed paths +drive the release; set the subject type deliberately. A squash touching two +plugins bumps both. ## Testing