external-pr-scope-guard.yml 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. name: External PR Scope Guard
  2. # Advisory check that surfaces what a NON-MEMBER pull request may change.
  3. # Members (write/admin) and the repo's own automation bot (bump SHA PRs) are unrestricted and
  4. # skip this check. For a non-member PR this fails unless the PR is an in-scope external
  5. # contribution per .github/scripts/external-pr-scope.js: it changes ONLY
  6. # .claude-plugin/marketplace.json, the delta is additions-only (no existing entry modified or
  7. # removed), and every ADDED entry's source.url is a repo that ALREADY backs a live plugin in
  8. # this marketplace (the allowed set is derived from the live marketplace — there is no
  9. # maintained allowlist).
  10. #
  11. # Do NOT add this job to branch protection as a required status check. The merge gate is the
  12. # `validate` + `scan` checks plus a maintainer approval; this guard is advisory signal for the
  13. # reviewer, not a hard gate. (Making it required would block the no-approval bump-merge path.)
  14. #
  15. # Security: runs on pull_request_target but checks out only the BASE repo (trusted) for the
  16. # shared script; the head marketplace.json is fetched as DATA via the API and parsed, never executed.
  17. on:
  18. pull_request_target:
  19. types: [opened, synchronize, reopened]
  20. permissions:
  21. contents: read
  22. pull-requests: read
  23. jobs:
  24. scope-guard:
  25. runs-on: ubuntu-latest
  26. steps:
  27. - uses: actions/checkout@v4 # base repo (trusted)
  28. - uses: actions/github-script@v7
  29. with:
  30. script: |
  31. const { evaluate, isExemptAuthor } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`);
  32. // Members (write/admin) and the repo's own automation bot (bump SHA PRs) are
  33. // unrestricted; only genuinely external contributions are scope-checked.
  34. const ex = await isExemptAuthor({ github, context });
  35. if (ex.exempt) {
  36. console.log(`${ex.reason} — scope guard not applicable.`);
  37. return;
  38. }
  39. const result = await evaluate({ github, context });
  40. if (!result.ok) {
  41. core.setFailed(
  42. `Scope guard: a non-member PR may only ADD marketplace.json entries whose source repo already backs a live plugin here.\n - ` +
  43. result.problems.join('\n - ')
  44. );
  45. return;
  46. }
  47. console.log(`Scope guard passed: adds ${result.added.join(', ') || 'none'}, all from repos already live here.`);