From f1c687e806a5ec4fac6660d6394311b0c870d7c3 Mon Sep 17 00:00:00 2001 From: fnrhombus <2511516+fnrhombus@users.noreply.github.com> Date: Mon, 18 May 2026 01:33:47 -0400 Subject: [PATCH] feat: release-please + auto-merge + marketplace-dispatch wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the standard plugin-release CI chain so plugins scaffolded from this template ship versions automatically on every PR merge: - `.github/workflows/release-please.yml` — opens release PRs on `feat:` / `fix:` commits, auto-merges them via PAT, creates tags, and fires a cross-repo dispatch to fnrhombus/claude-plugins' update-marketplace.yml so the new tag lands in the marketplace within seconds (vs. daily cron). - `.github/workflows/auto-merge.yml` — enables auto-merge on every non-draft PR. - `release-please-config.json` — `release-type: simple` with `.claude-plugin/plugin.json` listed as an extra-file so its `version` field tracks the release tag. Language-agnostic; plugins with a `package.json` (or other manifests) can add them as additional extra-files entries. - `.release-please-manifest.json` — starts at 0.1.0. Also: - Drops `"hooks": "./hooks/hooks.json"` from the template plugin.json. Claude Code auto-loads hooks/hooks.json; listing it manually triggers a duplicate-load error. - Updates CLAUDE.md: replaces the manual "bump version + tag + dispatch" publishing steps with a description of the now-automated flow, adds an AUTOMERGE_PAT secret to the first-time-setup checklist, and explains why a PAT is needed (workflow chaining + cross-repo dispatch). --- .claude-plugin/plugin.json | 3 +- .github/workflows/auto-merge.yml | 38 +++++++++++++++++++++ .github/workflows/release-please.yml | 49 ++++++++++++++++++++++++++++ .release-please-manifest.json | 3 ++ CLAUDE.md | 29 ++++++++-------- release-please-config.json | 15 +++++++++ 6 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index dcadaee..250e1e0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" } diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..bad2f6e --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -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" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..f5b2d62 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -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 }}" diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/CLAUDE.md b/CLAUDE.md index 4a8137b..a39760a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ""`. +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. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..d4b7717 --- /dev/null +++ b/release-please-config.json @@ -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" + } + ] + } + } +}