Auto-bump changed plugin patch versions workflow (jeremylongshore/claude-code-plugins-plus-skills)
The Auto-bump changed plugin patch versions workflow from jeremylongshore/claude-code-plugins-plus-skills, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Auto-bump changed plugin patch versions workflow from the jeremylongshore/claude-code-plugins-plus-skills repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Auto-bump changed plugin patch versions
# Runs on every pull_request event. Detects which plugin directories the PR
# touches (any file other than the plugin's own package.json), bumps each
# affected plugin's patch version (1.1.0 → 1.1.1), and commits the bump back
# to the PR branch. On merge to main, `publish-changed-packages.yml`
# republishes each bumped plugin and creates the per-package tag + release.
#
# This is the autonomous half of the release engineering pipeline:
# * Engineer pushes a code change → auto-bump fires → CI commits +N patch
# bumps to the PR branch.
# * Engineer reviews the bump in the PR diff, can override (write a manual
# minor or major bump in the same PR; the auto-bumper skips PRs where
# the only file changed in a plugin dir is its own package.json).
# * Merge to main → publish-changed-packages.yml ships the new versions.
#
# Why patch by default: every PR is treated as a non-breaking incremental
# release. Major / minor bumps remain a deliberate human choice - the
# engineer edits the version manually (or in a follow-up commit) and the
# auto-bumper steps aside. This avoids needing conventional-commits
# discipline across 400+ plugins while keeping per-PR patch traceability.
#
# Skip conditions:
# - PR head is a FORK (`head.repo.full_name != github.repository`). Fork PRs
# run with a read-only token and their branch lives in the fork, so the
# bump's checkout+push-back cannot work - it would only red-fail an otherwise
# good external contribution. Skip cleanly instead (maintainer bumps at merge).
# - PR head branch starts with `automation/` (avoids self-bumping the
# automation/npm-stats and similar generated PRs).
# - PR title or body contains `[skip auto-bump]`.
#
# Runs on `pull_request` (NOT pull_request_target) BY DESIGN: the bump needs a
# write token to commit back to the PR branch, and pull_request_target would hand
# that token to fork-PR code - a code-execution-with-secrets risk not worth a
# patch bump. The accepted cost is that a first-time fork contributor's run sits
# in the "Approve and run" queue until a maintainer approves it; since the job
# skips forks anyway (first skip condition above), approving it just lets it
# no-op cleanly. Fixing the queue entry would require raising the CI blast
# radius, so it stays.
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'plugins/**'
- 'packages/**'
permissions:
contents: write # commit bump back to PR branch
pull-requests: read
concurrency:
# One auto-bump run per PR; cancel an earlier run if a new commit arrives.
group: auto-bump-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
bump:
runs-on: ubuntu-latest
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
!startsWith(github.head_ref, 'automation/') &&
!contains(github.event.pull_request.title, '[skip auto-bump]') &&
!contains(github.event.pull_request.body, '[skip auto-bump]')
steps:
- name: Checkout PR head with full history
uses: actions/checkout@v6
with:
# Push to the PR head branch directly. `pull_request` events run
# against the merge ref by default, which can't be pushed to.
ref: ${{ github.head_ref }}
# Need full history so the merge-base diff against origin/<base>
# resolves cleanly.
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v6
with:
node-version: 20
- name: Compute and apply patch bumps
id: bump
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
# Make sure origin/<base> is fetched (full-history clone above
# should have it, but be explicit).
git fetch origin "${GITHUB_BASE_REF}":"refs/remotes/origin/${GITHUB_BASE_REF}" --quiet || true
# Capture the script's stdout for the PR comment, but exit-code is
# what gates the next step.
OUTPUT=$(node scripts/auto-bump-changed-plugins.mjs)
echo "$OUTPUT"
# Detect whether anything changed on disk.
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No bumps needed."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$OUTPUT" | grep -cE '→' || true)
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
fi
- name: Commit + push to PR branch
if: steps.bump.outputs.changed == 'true'
# `--no-verify` skips local pre-commit hooks (husky / lint-staged)
# which can OOM on prettier across the staged set in CI. Same
# pattern as `update-npm-stats.yml`.
env:
HEAD_REF: ${{ github.head_ref }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit --no-verify -m "chore(release): bump patch versions for ${{ steps.bump.outputs.count }} plugin(s)
Auto-generated by .github/workflows/auto-bump-on-pr.yml.
jeremy made me do it
-claude"
git push origin HEAD:"$HEAD_REF"
- name: Summary
if: always()
env:
HEAD_REF: ${{ github.head_ref }}
run: |
echo "## 🔢 Auto-bump on PR" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ steps.bump.outputs.changed }}" = "true" ]; then
echo "✅ Bumped ${{ steps.bump.outputs.count }} plugin(s); commit pushed to \`$HEAD_REF\`." >> "$GITHUB_STEP_SUMMARY"
else
echo "✓ No plugin source changes that warrant a patch bump." >> "$GITHUB_STEP_SUMMARY"
fi
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
name: Auto-bump changed plugin patch versions # Runs on every pull_request event. Detects which plugin directories the PR # touches (any file other than the plugin's own package.json), bumps each # affected plugin's patch version (1.1.0 → 1.1.1), and commits the bump back # to the PR branch. On merge to main, `publish-changed-packages.yml` # republishes each bumped plugin and creates the per-package tag + release. # # This is the autonomous half of the release engineering pipeline: # * Engineer pushes a code change → auto-bump fires → CI commits +N patch # bumps to the PR branch. # * Engineer reviews the bump in the PR diff, can override (write a manual # minor or major bump in the same PR; the auto-bumper skips PRs where # the only file changed in a plugin dir is its own package.json). # * Merge to main → publish-changed-packages.yml ships the new versions. # # Why patch by default: every PR is treated as a non-breaking incremental # release. Major / minor bumps remain a deliberate human choice - the # engineer edits the version manually (or in a follow-up commit) and the # auto-bumper steps aside. This avoids needing conventional-commits # discipline across 400+ plugins while keeping per-PR patch traceability. # # Skip conditions: # - PR head is a FORK (`head.repo.full_name != github.repository`). Fork PRs # run with a read-only token and their branch lives in the fork, so the # bump's checkout+push-back cannot work - it would only red-fail an otherwise # good external contribution. Skip cleanly instead (maintainer bumps at merge). # - PR head branch starts with `automation/` (avoids self-bumping the # automation/npm-stats and similar generated PRs). # - PR title or body contains `[skip auto-bump]`. # # Runs on `pull_request` (NOT pull_request_target) BY DESIGN: the bump needs a # write token to commit back to the PR branch, and pull_request_target would hand # that token to fork-PR code - a code-execution-with-secrets risk not worth a # patch bump. The accepted cost is that a first-time fork contributor's run sits # in the "Approve and run" queue until a maintainer approves it; since the job # skips forks anyway (first skip condition above), approving it just lets it # no-op cleanly. Fixing the queue entry would require raising the CI blast # radius, so it stays. on: pull_request: types: [opened, synchronize, reopened] paths: - 'plugins/**' - 'packages/**' permissions: contents: write # commit bump back to PR branch pull-requests: read concurrency: # One auto-bump run per PR; cancel an earlier run if a new commit arrives. group: auto-bump-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: bump: timeout-minutes: 30 runs-on: latchkey-small if: | github.event.pull_request.head.repo.full_name == github.repository && !startsWith(github.head_ref, 'automation/') && !contains(github.event.pull_request.title, '[skip auto-bump]') && !contains(github.event.pull_request.body, '[skip auto-bump]') steps: - name: Checkout PR head with full history uses: actions/checkout@v6 with: # Push to the PR head branch directly. `pull_request` events run # against the merge ref by default, which can't be pushed to. ref: ${{ github.head_ref }} # Need full history so the merge-base diff against origin/<base> # resolves cleanly. fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v6 with: cache: 'npm' node-version: 20 - name: Compute and apply patch bumps id: bump env: GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref }} run: | set -euo pipefail # Make sure origin/<base> is fetched (full-history clone above # should have it, but be explicit). git fetch origin "${GITHUB_BASE_REF}":"refs/remotes/origin/${GITHUB_BASE_REF}" --quiet || true # Capture the script's stdout for the PR comment, but exit-code is # what gates the next step. OUTPUT=$(node scripts/auto-bump-changed-plugins.mjs) echo "$OUTPUT" # Detect whether anything changed on disk. if git diff --quiet; then echo "changed=false" >> "$GITHUB_OUTPUT" echo "No bumps needed." else echo "changed=true" >> "$GITHUB_OUTPUT" COUNT=$(echo "$OUTPUT" | grep -cE '→' || true) echo "count=$COUNT" >> "$GITHUB_OUTPUT" fi - name: Commit + push to PR branch if: steps.bump.outputs.changed == 'true' # `--no-verify` skips local pre-commit hooks (husky / lint-staged) # which can OOM on prettier across the staged set in CI. Same # pattern as `update-npm-stats.yml`. env: HEAD_REF: ${{ github.head_ref }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A git commit --no-verify -m "chore(release): bump patch versions for ${{ steps.bump.outputs.count }} plugin(s) Auto-generated by .github/workflows/auto-bump-on-pr.yml. jeremy made me do it -claude" git push origin HEAD:"$HEAD_REF" - name: Summary if: always() env: HEAD_REF: ${{ github.head_ref }} run: | echo "## 🔢 Auto-bump on PR" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" if [ "${{ steps.bump.outputs.changed }}" = "true" ]; then echo "✅ Bumped ${{ steps.bump.outputs.count }} plugin(s); commit pushed to \`$HEAD_REF\`." >> "$GITHUB_STEP_SUMMARY" else echo "✓ No plugin source changes that warrant a patch bump." >> "$GITHUB_STEP_SUMMARY" fi
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.