diff --git a/.github/workflows/bump-plugin-shas.yml b/.github/workflows/bump-plugin-shas.yml index dab017ea..4fb48e57 100644 --- a/.github/workflows/bump-plugin-shas.yml +++ b/.github/workflows/bump-plugin-shas.yml @@ -2,25 +2,24 @@ name: Bump Plugin SHAs # Nightly sweep: for each external entry whose upstream HEAD has moved past # its pinned SHA, validate at the new SHA with `claude plugin validate` -# inline, then open one PR with all passing bumps. Each run force-resets the -# bump/plugin-shas branch, so a previous night's unmerged PR is replaced (and -# its review state discarded) — review and merge same-day to avoid churn. +# inline, then open one PR per bumped plugin on branch `bump/`. +# Failing entries stay isolated in their own PR; passing bumps merge +# independently. # # Bot-free — uses the default GITHUB_TOKEN. PRs opened with GITHUB_TOKEN don't -# trigger on:pull_request workflows, so the policy scan (`Scan Plugins`, a -# required status check on main) would never run and the bump PR could never -# merge. workflow_dispatch is exempt from that recursion guard, so we dispatch -# the scan ourselves on the bump branch after the PR is opened. The check run -# lands on the branch HEAD — the same SHA as the PR head — and satisfies the -# required check. +# trigger on:pull_request workflows, so the required status checks on main +# (`scan` from Scan Plugins, `check` from Check MCP URLs, `validate` from +# Validate Plugins) would never run and the bump PR could never merge. +# workflow_dispatch is exempt from that recursion guard, so we dispatch all +# three ourselves against each per-entry bump branch after its PR is opened. +# Each check run lands on the branch HEAD — the same SHA as the PR head — and +# satisfies the corresponding required check. (Each of those workflows runs +# its job unconditionally on workflow_dispatch, so a dispatch always reports.) # -# max-bumps is set above the external-entry count so a single run can clear -# any backlog. The cost-control mechanisms are downstream: -# - scan-plugins.yml caches verdicts by (plugin, sha) so an unchanged SHA -# is never re-scanned across nightly force-resets. -# - revert-failed-bumps.yml drops policy-failing entries from the bump PR -# so one bad upstream can't block the rest. -# See those files for details. +# max-bumps caps the per-night work for cost control. Per-entry scans are +# more expensive than a single batched scan, so the cap is conservative. +# The composite action skips entries that already have an open bump PR, so +# re-dispatches don't pile up duplicate work. on: schedule: @@ -30,12 +29,12 @@ on: max_bumps: description: Cap on plugins bumped this run required: false - default: '130' + default: '30' permissions: contents: write pull-requests: write - actions: write # gh workflow run scan-plugins.yml on the bump branch + actions: write # gh workflow run {scan-plugins,check-mcp-urls,validate-plugins}.yml per bump branch concurrency: group: bump-plugin-shas @@ -43,8 +42,8 @@ concurrency: jobs: bump: runs-on: ubuntu-latest - # Per-bump cost is ~2s (ls-remote + shallow clone + validate); 130 entries - # is ~5 min. The 60 min ceiling absorbs slow upstreams without letting a + # Per-bump cost is ~2s (ls-remote + shallow clone + validate); 30 entries + # is ~1-2 min. The 60 min ceiling absorbs slow upstreams without letting a # pathological run consume the default 360 min budget. timeout-minutes: 60 steps: @@ -52,18 +51,44 @@ jobs: # createCommitOnBranch-based bump so commits are signed by GitHub and # satisfy the org-level required_signatures ruleset on main. - - uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@c41c6911de0afffd2bc5cd8b21fb1e06444ee13b + - uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@e2019b2a01f11aa1484c53540b1cfab5eebbc299 id: bump with: marketplace-path: .claude-plugin/marketplace.json - max-bumps: ${{ inputs.max_bumps || '130' }} + max-bumps: ${{ inputs.max_bumps || '30' }} + pr-mode: per-entry claude-cli-version: latest - # `bump/plugin-shas` is the action's default `pr-branch`. The scan diffs - # the branch against origin/main (the action's base-ref fallback when - # there's no pull_request event) and scans only the bumped entries. - - name: Dispatch policy scan on bump branch - if: steps.bump.outputs.pr-url != '' + # Per-entry fan-out: dispatch the three required checks against each bump + # branch. `pr-urls` is a JSON array of {name, old_sha, new_sha, branch, + # pr_url} entries emitted by the composite action when pr-mode is + # per-entry. All three (scan / check / validate) are required on main and + # none fire on the GITHUB_TOKEN-opened PR, so each must be dispatched. + # A single failed dispatch (transient API error / rate limit) must not + # strand the remaining branches, so we attempt every dispatch, then fail + # the step if any failed: a missing required check would otherwise leave + # its bump PR silently blocked behind a green run, and the composite + # action skips slugs with an open PR so it would never be retried. + - name: Dispatch required checks per per-entry PR + if: steps.bump.outputs.pr-urls != '' && steps.bump.outputs.pr-urls != '[]' env: GH_TOKEN: ${{ github.token }} - run: gh workflow run scan-plugins.yml --ref bump/plugin-shas + PR_URLS: ${{ steps.bump.outputs.pr-urls }} + run: | + set -euo pipefail + dispatch_failures="$(mktemp)" + jq -c '.[]' <<<"$PR_URLS" | while read -r entry; do + branch=$(jq -r '.branch' <<<"$entry") + name=$(jq -r '.name' <<<"$entry") + for wf in scan-plugins check-mcp-urls validate-plugins; do + echo "Dispatching ${wf}.yml against $branch ($name)" + if ! gh workflow run "${wf}.yml" --ref "$branch"; then + echo "::error::Failed to dispatch ${wf}.yml against $branch ($name) — required check will be missing; re-dispatch with: gh workflow run ${wf}.yml --ref $branch" + echo "${wf} ${branch}" >> "$dispatch_failures" + fi + done + done + if [ -s "$dispatch_failures" ]; then + echo "::error::$(wc -l < "$dispatch_failures" | tr -d ' ') required-check dispatch(es) failed; the affected bump PR(s) are blocked until re-dispatched (see annotations above)." + exit 1 + fi diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index b297bdea..798cde5d 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -12,6 +12,14 @@ on: branches: [main] paths: - '.claude-plugin/**' + # `validate` is a required status check on main. Bump PRs are opened with + # GITHUB_TOKEN, which doesn't fire on:pull_request (recursion guard), so the + # path-filtered trigger above never reports on them and the PR would be + # blocked forever. The bump workflow dispatches this against each per-entry + # bump branch instead; the check run lands on the branch HEAD (= PR head) + # and satisfies the required check. The validate job runs unconditionally, + # so a dispatch always reports. + workflow_dispatch: permissions: contents: read