Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions .github/workflows/automated-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<plugin>/ (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(<plugin>)! / BREAKING CHANGE -> major
# feat(<plugin>) -> minor
# fix|perf|refactor(<plugin>) -> 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
Expand Down Expand Up @@ -108,13 +112,35 @@ jobs:
scope_re = re.compile(
r'^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<bang>!)?:\s*(?P<desc>.+)$')

# A commit qualifies for this plugin if it changed
# release-worthy content under plugins/<plugin>/ (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()
Expand All @@ -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)
Expand Down
31 changes: 19 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(<plugin>)!:` or body `BREAKING CHANGE:` | Major bump for `<plugin>` |
| `feat(<plugin>): ...` | Minor bump for `<plugin>` |
| `fix(<plugin>): ...`, `perf(<plugin>):`, `refactor(<plugin>):` | 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/<plugin>/` - anything except `tests/` and the CI-managed
`CHANGELOG.md` - **OR** its subject is explicitly scoped to the plugin
(`feat(<plugin>): ...`, back-compat). The conventional-commit **type** of the
qualifying commits then sets the bump:

| Qualifying commit type | Effect |
|------------------------|--------|
| `feat!:` / `feat(<plugin>)!:` / 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`,
Expand Down
31 changes: 18 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(<plugin>): ...` | minor bump for `<plugin>` |
| `fix(<plugin>): ...` / `perf` / `refactor` | patch bump |
| `feat(<plugin>)!: ...` 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/<plugin>/` (anything except `tests/` and the CI-managed
`CHANGELOG.md`), or when its subject is scoped to that plugin
(`feat(<plugin>): ...`, 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

Expand Down