mirror of
https://github.com/lucas-labs/claude-plugins.git
synced 2026-07-10 19:25:44 -03:00
* feat(ci): allow live external contributors to open scoped PRs Add an opt-in allowlist so a vetted external developer who already has a plugin live in this marketplace — but cannot use the submission form (e.g. an enterprise partner without a Claude account) — can open a reviewable PR instead of having it auto-closed. - .github/external-contributors.json: username -> allowed_sources map (doubles as the allowlist and the per-author source scope). - close-external-prs.yml: skip the auto-close for allowlisted authors (reads the list from the trusted base checkout). Grants ONLY the right to open a PR; CI + maintainer approval are unchanged. - external-pr-scope-guard.yml: required check for allowlisted external authors. Fails unless the PR touches ONLY marketplace.json and the delta is additions-only, with every added entry's source.url under that author's allowed_sources. Anthropic members are unrestricted. Reads head marketplace.json as data via the API (no untrusted checkout). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(ci): neutral wording in external-contributors allowlist note Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ci): key external-PR allowlist on source org, not individuals Replace the per-username allowlist with a source-org allowlist so no individual is named in the repo. A non-member PR stays open only if it adds marketplace.json entries whose source.url is under an allowlisted prefix and changes nothing else; merge still requires CI + maintainer approval. - external-pr-allowed-sources.json: flat allowed_sources prefixes (no usernames) - scripts/external-pr-scope.js: shared additions-only / allowed-source logic - close-external-prs.yml + external-pr-scope-guard.yml: both use the shared module Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ci): derive external-PR scope from live repos, no maintained list Replace the curated source-org allowlist with auto-derivation from the live marketplace. A non-member PR stays open only if it ADDS entries whose source.url repo ALREADY backs a live plugin here, additions-only, nothing else touched. No list to maintain, no individuals named. Trust is anchored in the source repo + pinned SHA (org-controlled), not the submitter's identity; merge still requires CI + maintainer approval. - remove external-pr-allowed-sources.json - scripts/external-pr-scope.js: derive allowed repos from base marketplace.json - both workflows drop the allowlist-file arg Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): diff external-PR scope against the merge-base, not base tip Comparing base-branch-tip -> head made a fork that is behind main report all of main's later additions as phantom removals/modifications, which would wrongly fail the scope guard for a legitimate additions-only PR. Diff merge-base -> head (the PR's actual changes) instead; keep the "already live" check against the current base branch. Found by an end-to-end run of evaluate() against real PR data (#3298 stayed clean; #3044's phantom drift collapsed to its real one-line change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
2.2 KiB
YAML
53 lines
2.2 KiB
YAML
name: External PR Scope Guard
|
|
|
|
# Required status check that constrains what a NON-MEMBER pull request may change.
|
|
# Members (write/admin) are unrestricted and skip this check. For a non-member PR this
|
|
# fails unless the PR is an in-scope external contribution per .github/scripts/external-pr-scope.js:
|
|
# it changes ONLY .claude-plugin/marketplace.json, the delta is additions-only (no existing
|
|
# entry modified or removed), and every ADDED entry's source.url is a repo that ALREADY backs
|
|
# a live plugin in this marketplace (the allowed set is derived from the live marketplace —
|
|
# there is no maintained allowlist).
|
|
#
|
|
# Add the scope-guard job as a REQUIRED status check in branch protection for it to block merge.
|
|
#
|
|
# Security: runs on pull_request_target but checks out only the BASE repo (trusted) for the
|
|
# shared script; the head marketplace.json is fetched as DATA via the API and parsed, never executed.
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, synchronize, reopened]
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
scope-guard:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4 # base repo (trusted)
|
|
- uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const author = context.payload.pull_request.user.login;
|
|
|
|
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
owner: context.repo.owner, repo: context.repo.repo, username: author,
|
|
});
|
|
if (['admin', 'write'].includes(perm.permission)) {
|
|
console.log(`${author} is ${perm.permission} (member) — scope guard not applicable.`);
|
|
return;
|
|
}
|
|
|
|
const { evaluate } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`);
|
|
const result = await evaluate({ github, context });
|
|
|
|
if (!result.ok) {
|
|
core.setFailed(
|
|
`Scope guard: a non-member PR may only ADD marketplace.json entries whose source repo already backs a live plugin here.\n - ` +
|
|
result.problems.join('\n - ')
|
|
);
|
|
return;
|
|
}
|
|
console.log(`Scope guard passed: adds ${result.added.join(', ') || 'none'}, all from repos already live here.`);
|