Skip to content
Latchkey

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.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: jeremylongshore/claude-code-plugins-plus-skills.github/workflows/auto-bump-on-pr.ymlLicense MITView source

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

workflow (.yml)
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

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow