external-pr-scope-guard.yml 2.2 KB

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