From 45896c8f2fe68e68ca2a8e3554d4441f48475431 Mon Sep 17 00:00:00 2001 From: Tobin South Date: Mon, 11 May 2026 13:14:33 -0700 Subject: [PATCH] Make Scan Plugins a viable required check; auto-dispatch on bump PRs (#1815) Scan Plugins is meant to gate every change to marketplace.json, but two gaps made that unenforceable: 1. The bump workflow opens PRs with GITHUB_TOKEN, which GitHub exempts from on:pull_request triggers. Weekly bump PRs (e.g. #1809) get no scan check at all. 2. The workflow had a paths filter, so a required-check ruleset for `scan` would block every PR that doesn't touch marketplace.json (no check run = pending forever). Fixes: scan-plugins.yml - Drop the paths filter; replace with a step-level `git diff --quiet` early-exit on the same paths. The check now reports on every PR, which makes it safe to require. - Fail closed when ANTHROPIC_API_KEY is unset and a scan is needed. The shared action no-ops gracefully in that case (right default for community repos), but a required check that silently does nothing is a rubber stamp. bump-plugin-shas.yml - After the action opens the bump PR, `gh workflow run scan-plugins.yml --ref bump/plugin-shas`. workflow_dispatch is exempt from the GITHUB_TOKEN recursion guard, and the resulting check run lands on the branch HEAD (= PR head), so it satisfies the required check. - Add `actions: write` so the dispatch is allowed. Follow-up: add a repo ruleset on main requiring the `scan` check (integration: github-actions) once this merges. --- .github/workflows/bump-plugin-shas.yml | 21 ++++++++++-- .github/workflows/scan-plugins.yml | 46 +++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/.github/workflows/bump-plugin-shas.yml b/.github/workflows/bump-plugin-shas.yml index a2d965f..77661a5 100644 --- a/.github/workflows/bump-plugin-shas.yml +++ b/.github/workflows/bump-plugin-shas.yml @@ -4,9 +4,13 @@ name: Bump Plugin SHAs # its pinned SHA, validate at the new SHA with `claude plugin validate` # inline, then open one PR with all passing bumps. # -# Bot-free — uses the default GITHUB_TOKEN. Because GITHUB_TOKEN-opened PRs -# don't trigger on:pull_request workflows, validation runs in this workflow -# before the PR is opened; the PR body links back here as the CI evidence. +# 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. on: schedule: @@ -21,6 +25,7 @@ on: permissions: contents: write pull-requests: write + actions: write # gh workflow run scan-plugins.yml on the bump branch concurrency: group: bump-plugin-shas @@ -34,7 +39,17 @@ 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 + id: bump with: marketplace-path: .claude-plugin/marketplace.json max-bumps: ${{ inputs.max_bumps || '20' }} 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 != '' + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run scan-plugins.yml --ref bump/plugin-shas diff --git a/.github/workflows/scan-plugins.yml b/.github/workflows/scan-plugins.yml index fc3f571..14bf9b4 100644 --- a/.github/workflows/scan-plugins.yml +++ b/.github/workflows/scan-plugins.yml @@ -1,10 +1,15 @@ name: Scan Plugins +# Claude policy scan of changed external marketplace entries. +# +# `scan` is a required status check on main. A path-filtered workflow never +# reports a check run when its paths don't match, which would leave unrelated +# PRs blocked forever — so this workflow runs on every PR and skips the heavy +# scan setup at the step level when nothing scan-relevant changed. The check +# always reports. + on: pull_request: - paths: - - '.claude-plugin/marketplace.json' - - '.github/policy/**' workflow_dispatch: inputs: scan_all: @@ -24,9 +29,42 @@ jobs: with: fetch-depth: 0 + # Same paths the workflow-level filter used to gate on. workflow_dispatch + # always runs the scan (no PR diff to inspect). + - name: Check for scan-relevant changes + id: changes + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + echo "relevant=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git diff --quiet "$BASE_SHA" HEAD -- .claude-plugin/marketplace.json .github/policy/; then + echo "relevant=false" >> "$GITHUB_OUTPUT" + echo "::notice::No changes to marketplace.json or policy/ — skipping policy scan." + else + echo "relevant=true" >> "$GITHUB_OUTPUT" + fi + + # The shared action no-ops gracefully when ANTHROPIC_API_KEY is unset + # (sensible default for community repos). Here `scan` is a required + # check, so a silent no-op would make it a rubber stamp — fail closed. + - name: Require ANTHROPIC_API_KEY when a scan is needed + if: steps.changes.outputs.relevant == 'true' + env: + API_KEY_SET: ${{ secrets.ANTHROPIC_API_KEY != '' }} + run: | + if [[ "$API_KEY_SET" != "true" ]]; then + echo "::error::ANTHROPIC_API_KEY is not configured; refusing to skip a required policy scan." + exit 1 + fi + # Blocking: policy failures fail the job. Loosen by removing # fail-on-findings if the false-positive rate is too high. - - uses: anthropics/claude-plugins-community/.github/actions/scan-plugins@b277757588871fe55b2620de8c6dfda470e2e9d8 + - if: steps.changes.outputs.relevant == 'true' + uses: anthropics/claude-plugins-community/.github/actions/scan-plugins@b277757588871fe55b2620de8c6dfda470e2e9d8 with: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} policy-prompt: .github/policy/prompt.md