A GitHub Action that compiles a pluginpack source repo for a single target and reconciles the generated output against a published plugin repo — by opening a pull request, never by pushing to the output repo's default branch.
It is the publish step for a source-of-truth layout: author plugins once in a
pluginpack source repo, then sync the native output into each ecosystem's
existing install surface (e.g. gleanwork/claude-plugins,
gleanwork/cursor-plugins) without moving the repos users install from.
- Check out the output repo into a temp dir (using
token). - Run
pluginpack diff --target <target> --against <checkout>. - Clean → report up to date, exit.
- Stale +
mode: check→ fail the job. This is the guard an output repo runs to keep hand-edits from drifting away from source. - Stale +
mode: sync→pluginpack build --out-dir <checkout>, commit to a machine-owned branchpluginpack/sync-<target>, force-push that branch, and open or update a PR intooutput-ref.
The sync branch is force-updated on every run (idempotent). The action never commits to the output repo's base branch — a human (or a separately configured auto-merge) merges the PR.
# In the source repo (e.g. agent-plugins), on release:jobs:
sync-claude:
runs-on: ubuntu-latestpermissions:
contents: readconcurrency:
group: pluginpack-sync-claudecancel-in-progress: truesteps:
# Mint a short-lived token scoped only to the output repo.
- uses: actions/create-github-app-token@v3id: app-tokenwith:
client-id: ${{ secrets.PLUGINPACK_CLIENT_ID }}private-key: ${{ secrets.PLUGINPACK_APP_KEY }}owner: gleanworkrepositories: claude-plugins
- uses: actions/checkout@v5
- uses: actions/setup-node@v5with:
node-version: 24
- uses: gleanwork/pluginpack-action@v1with:
target: claudeoutput-repo: gleanwork/claude-pluginstoken: ${{ steps.app-token.outputs.token }}The action needs a token with contents: write and
pull_requests: write on each output repo — to push the machine-owned sync
branch and open the PR. It never needs write access to the source repo. Provide
it one of two ways.
Short-lived, least-privilege, and not tied to a person.
- Create a GitHub App in the org (Settings → Developer settings → GitHub Apps → New). Under Repository permissions grant Contents: Read and write and Pull requests: Read and write. No webhook needed.
- Install the App on the output repos (e.g.
claude-plugins,cursor-plugins). - In the source repo (where this action runs) add two Actions secrets:
PLUGINPACK_CLIENT_ID— the App's Client ID (on the App's General page, e.g.Iv23li…).create-github-app-tokenv3 usesclient-id; the numeric App ID is deprecated.PLUGINPACK_APP_KEY— a generated private key (the full.pemcontents).
- Mint a per-run token with
actions/create-github-app-token, scoped to just the target output repo, and pass it totoken(see Usage).
The Client ID isn't sensitive, so a repo variable works for it too; we keep
both as secrets so all App credentials live in one place. The names are
arbitrary — match them to whatever your create-github-app-token step
references.
A fine-grained PAT with Contents: Read and write and Pull requests: Read and write on the output repos, stored as a secret. Drop the app-token step and pass the secret straight through:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5with:
node-version-file: .node-version
- uses: gleanwork/pluginpack-action@v1with:
target: claudeoutput-repo: gleanwork/claude-pluginstoken: ${{ secrets.PLUGINPACK_SYNC_TOKEN }}A PAT is tied to its creator and longer-lived than an App token — rotate it periodically.
| Input | Required | Default | Description |
|---|---|---|---|
target | yes | — | pluginpack target to build (claude, cursor, …). |
output-repo | yes | — | Published repo to sync into, as owner/name. |
output-ref | no | main | Base branch the sync PR targets. |
token | yes | — | Token with contents:write + pull-requests:write on the output repo. |
mode | no | sync | sync opens/updates a PR when stale; check fails when stale. |
pluginpack-version | no | latest | npm version/dist-tag of @gleanwork/pluginpack. |
working-directory | no | . | Path to the checked-out pluginpack source repo. |
pr-branch-prefix | no | pluginpack/sync | Prefix for the sync branch; -<target> is appended. |
pr-labels | no | pluginpack-sync | Comma-separated labels for the sync PR. |
| Output | Description |
|---|---|
stale | "true" when the output repo differed from a fresh build. |
pr-number | Number of the opened/updated sync PR (sync mode). |
pr-url | HTML URL of the sync PR (sync mode). |
This action follows the
actions/typescript-action
conventions: TypeScript in src/, bundled to dist/ with Rollup, tested with
Vitest. Run npm run all (format, lint, test, package) before committing; the
committed dist/ is what runs, so check-dist.yml enforces it stays current.