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
3 changes: 1 addition & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
},
"homepage": "https://github.com/fnrhombus/TODO-repo-name",
"repository": "https://github.com/fnrhombus/TODO-repo-name",
"license": "MIT",
"hooks": "./hooks/hooks.json"
"license": "MIT"
}
38 changes: 38 additions & 0 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: auto-merge

# Enable auto-merge (squash) on every non-draft PR as soon as it opens.
# Branch protection on main holds the actual merge until any required
# status checks are green; with no required checks, --auto degrades to an
# immediate merge once the PR is mergeable.
#
# This workflow itself is NOT in the required-status-checks list — its
# success isn't a gate; it just registers intent to merge. If it fails
# (transient network, etc.), the PR is unblocked but auto-merge isn't
# enabled, and the author can enable it manually.

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: write
pull-requests: write

jobs:
enable-auto-merge:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Enable auto-merge (squash)
env:
# Using a PAT (not GITHUB_TOKEN) so the eventual merge fires
# downstream workflows. GitHub intentionally suppresses workflow
# triggers for GITHUB_TOKEN-actored events to prevent loops; that
# suppression breaks the release-please chain (no release PR
# opened after a feature PR merges).
GH_TOKEN: ${{ secrets.AUTOMERGE_PAT }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# --repo is required: gh otherwise tries to detect the repo from a
# local git checkout, but this job has none and bails with
# "fatal: not a git repository". Pass GITHUB_REPOSITORY explicitly.
run: gh pr merge --auto --squash --repo "$GITHUB_REPOSITORY" "$PR_NUMBER"
49 changes: 49 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: release-please

on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
# PAT (not GITHUB_TOKEN) so the release PR's pull_request.opened
# event fires auto-merge.yml. GITHUB_TOKEN-created PRs don't
# trigger downstream workflows, which would leave the release PR
# sitting un-merged.
token: ${{ secrets.AUTOMERGE_PAT }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# Auto-merge the release PR so feature PR → release PR → tag is a
# single user-visible merge. The PAT-created PR triggers auto-merge.yml
# which enables auto-merge from the PR side; this is a safety net.
- name: Auto-merge release PR
if: ${{ steps.release.outputs.pr }}
env:
GH_TOKEN: ${{ secrets.AUTOMERGE_PAT }}
run: |
pr_number=$(echo '${{ steps.release.outputs.pr }}' | jq -r .number)
gh pr merge "$pr_number" --auto --squash --repo "$GITHUB_REPOSITORY"

# When release-please actually created a release (release_created=true
# on its second run, after the release PR is merged and the tag is
# cut), dispatch the marketplace's rebuild workflow so the new plugin
# tag/version lands in fnrhombus/claude-plugins' marketplace.json within
# seconds instead of waiting for its daily cron.
- name: Trigger marketplace rebuild
if: ${{ steps.release.outputs.release_created == 'true' }}
env:
GH_TOKEN: ${{ secrets.AUTOMERGE_PAT }}
run: |
gh workflow run update-marketplace.yml \
--repo fnrhombus/claude-plugins
echo "dispatched marketplace rebuild after release ${{ steps.release.outputs.tag_name }}"
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
29 changes: 16 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,33 @@ After cloning the new repo:
5. **Update `hooks/hooks.json`** if the hook event or matcher is different from the default `PreToolUse` / `Bash`.
6. **Rewrite `README.md`** as *advertising* — lead with the problem, show the before/after, keep install minimal. See `fnrhombus/claude-code-pathfix` for a reference.
7. **Add the `claude-code-plugin` topic** to the repo: `gh repo edit --add-topic claude-code-plugin`. This is how `fnrhombus/claude-plugins` (the central marketplace) discovers the plugin — without this topic, the plugin will never show up in `/plugin install`.
8. **Commit + push** to `main`.
8. **Add the `AUTOMERGE_PAT` repository secret.** Required for the release-please + auto-merge + marketplace-dispatch chain. The PAT needs `repo` + `workflow` scope, and `workflow` scope on `fnrhombus/claude-plugins` so the dispatch step can fire the marketplace rebuild. `gh secret set AUTOMERGE_PAT --body "<token>"`.
9. **Commit + push** to `main`.

## Publishing a new version

1. Bump the `version` in both `package.json` and `.claude-plugin/plugin.json` (keep them in sync).
2. Commit, tag (`git tag v0.2.0 && git push --tags`), let CI publish to npm.
3. **Refresh the marketplace** so users see the new version within minutes instead of waiting for the daily cron:
Use [conventional commits](https://www.conventionalcommits.org/). `feat:` bumps minor, `fix:` bumps patch, `feat!:` (or `BREAKING CHANGE:` in the body) bumps major. `docs:`, `refactor:`, `chore:`, `ci:` don't bump.

```bash
gh workflow run update-marketplace.yml --repo fnrhombus/claude-plugins
```
The flow is fully automatic:

This triggers the marketplace repo's `update-marketplace.yml` workflow manually. It will re-scan all `claude-code-plugin`-tagged repos (including this one), read the updated `plugin.json`, and commit a refreshed `marketplace.json` to itself.
1. Merge a `feat:` or `fix:` PR to `main`.
2. `release-please.yml` opens (or updates) a `chore(main): release vX.Y.Z` PR with the proposed bump + auto-generated `CHANGELOG.md` entry.
3. `auto-merge.yml` enables auto-merge on that PR; once required checks pass it merges.
4. The merge triggers release-please's second run, which creates the tag and a GitHub Release.
5. The same workflow then dispatches `update-marketplace.yml` on `fnrhombus/claude-plugins`, so the new version lands in the marketplace within seconds.

Alternatively, wait — the marketplace refreshes on a daily cron anyway. The manual trigger is only useful if you want the new version visible immediately.
No manual version bumps, no manual tagging, no manual marketplace pokes.

## Why the indirect flow?
## Why a PAT?

GitHub's `GITHUB_TOKEN` is scoped to its own repo, so this plugin's CI can't push to `fnrhombus/claude-plugins` directly. Cross-repo pushes would need a Personal Access Token stored as a secret in every plugin repo — annoying to set up once per plugin, and a long-lived credential to manage.
Two cross-trigger requirements force the use of `AUTOMERGE_PAT`:

Instead, the marketplace *pulls* from plugin repos on a schedule using its own `GITHUB_TOKEN` (which naturally has write access to itself). Plugin repos don't need any secrets or auth; they just need the `claude-code-plugin` topic and a valid `.claude-plugin/plugin.json` on their default branch.
1. **In-repo workflow chaining.** GitHub suppresses workflow runs for `GITHUB_TOKEN`-actored events to prevent loops. The release PR has to be opened by a PAT-actored event so its merge can trigger downstream workflows (the tag-creating second run of release-please, plus this repo's `test`/`build` jobs if any).
2. **Cross-repo dispatch.** `GITHUB_TOKEN` is scoped to its own repo; it can't dispatch a workflow on `fnrhombus/claude-plugins`. The PAT bridges that boundary so the marketplace rebuild fires immediately on release instead of waiting for its daily cron.

## What NOT to do

- **Don't hand-edit `fnrhombus/claude-plugins/.claude-plugin/marketplace.json`.** The cron overwrites it on every run. Update the source (`.claude-plugin/plugin.json` in *this* repo) and let the cron propagate.
- **Don't hand-edit `fnrhombus/claude-plugins/.claude-plugin/marketplace.json`.** Both the daily cron and the per-release dispatch overwrite it. Update the source (`.claude-plugin/plugin.json` here, bumped automatically by release-please) and let the dispatch propagate.
- **Don't put `"hooks": "./hooks/hooks.json"` in `.claude-plugin/plugin.json`.** Claude Code auto-loads `hooks/hooks.json`; listing it again causes a duplicate-load error. The `hooks` field is only for *additional* hook files beyond the standard one.
- **Don't forget the `claude-code-plugin` topic.** Without it, the marketplace has no way to discover the repo, and users can't install the plugin.
- **Don't skip the `dist/` commit.** Plugins distributed via `/plugin install` are served directly from the GitHub repo contents — there's no build step on the user side. If this plugin has a build step (tsup, etc.), commit `dist/` alongside `src/` so the plugin hook can actually run what it advertises.
15 changes: 15 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "simple",
"extra-files": [
{
"type": "json",
"path": ".claude-plugin/plugin.json",
"jsonpath": "$.version"
}
]
}
}
}
Loading