mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-26 13:53:29 +00:00
Compare commits
34 Commits
external-c
...
dev/scope-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beb6ec5d10 | ||
|
|
63f2b164fb | ||
|
|
c8e9219efb | ||
|
|
6b93bc00d3 | ||
|
|
16c1372836 | ||
|
|
ff23096dcd | ||
|
|
06c6d8878b | ||
|
|
324d8ebe73 | ||
|
|
c0236a0ffd | ||
|
|
a8237e1537 | ||
|
|
0dcbdd33a2 | ||
|
|
c39d4bb19a | ||
|
|
286e9f2217 | ||
|
|
28112e303c | ||
|
|
805cb8ec16 | ||
|
|
02540bfb49 | ||
|
|
d96849b22e | ||
|
|
60d98420a3 | ||
|
|
0c47f2e0b8 | ||
|
|
ec08d81de6 | ||
|
|
3b5666f0aa | ||
|
|
7a5090872c | ||
|
|
9679408140 | ||
|
|
8b6c3cc8f7 | ||
|
|
a837737d47 | ||
|
|
8ac5884387 | ||
|
|
d5f8b26fe2 | ||
|
|
da79cb089b | ||
|
|
ecdac18ae0 | ||
|
|
26461e1116 | ||
|
|
976a80e08f | ||
|
|
38cd9fc7c2 | ||
|
|
6e2d1f2f91 | ||
|
|
4976ad9034 |
@@ -57,7 +57,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/SalesforceAIResearch/agentforce-adlc.git",
|
||||
"sha": "772aaa20ebdd97736a94ebcd9d60fd3949342b60"
|
||||
"sha": "2b2f59d9f29d3dae3a4bec0b953237f03bbba7af"
|
||||
},
|
||||
"homepage": "https://github.com/SalesforceAIResearch/agentforce-adlc"
|
||||
},
|
||||
@@ -77,7 +77,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/AikidoSec/aikido-claude-plugin.git",
|
||||
"sha": "01e8cf542500e579cff948a0fa0365e4f819d7b4"
|
||||
"sha": "fbe11e287175e5eda448516dd2f741a63b276514"
|
||||
},
|
||||
"homepage": "https://github.com/AikidoSec/aikido-claude-plugin"
|
||||
},
|
||||
@@ -223,7 +223,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "e4ebf9a7ad3f8dbf3fcfda9c245a65eb1415967b"
|
||||
"sha": "ed2fe757381ff42337fd7bce56a50f31134d9dce"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -353,7 +353,7 @@
|
||||
"url": "https://github.com/aws/agent-toolkit-for-aws.git",
|
||||
"path": "plugins/aws-data-analytics",
|
||||
"ref": "main",
|
||||
"sha": "ff1dc6f45f5203147f6cd52662cc74ded4bb0825"
|
||||
"sha": "49ca75209219a89ac43690d6ca0a59d976933ee8"
|
||||
},
|
||||
"homepage": "https://github.com/aws/agent-toolkit-for-aws"
|
||||
},
|
||||
@@ -521,7 +521,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/brightdata/skills.git",
|
||||
"sha": "8d427e9871566efe3f0a1c8888f98b6fe8288831"
|
||||
"sha": "e825f02fbcd7a89087fd1053a57ddcd45113370f"
|
||||
},
|
||||
"homepage": "https://docs.brightdata.com"
|
||||
},
|
||||
@@ -981,7 +981,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/dash0hq/dash0-agent-plugin.git",
|
||||
"sha": "f8c31f6fcdc6588a27153ceed09e561a40da3a86"
|
||||
"sha": "fb9a6207929e5fc45c2661e5c74a2e077b3de79d"
|
||||
},
|
||||
"homepage": "https://dash0.com/"
|
||||
},
|
||||
@@ -992,7 +992,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "e4ebf9a7ad3f8dbf3fcfda9c245a65eb1415967b"
|
||||
"sha": "ed2fe757381ff42337fd7bce56a50f31134d9dce"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "e4ebf9a7ad3f8dbf3fcfda9c245a65eb1415967b"
|
||||
"sha": "ed2fe757381ff42337fd7bce56a50f31134d9dce"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/databases-on-aws",
|
||||
"ref": "main",
|
||||
"sha": "66dd3cf5acdf374cc0d79af2bf51fa6fbb975c07"
|
||||
"sha": "96a073a195491f2192c256ba66730b631ced03e1"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -1087,7 +1087,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/gemini-cli-extensions/dataproc.git",
|
||||
"sha": "c36c7f8bb53a1f8903382471366986ef226c509d"
|
||||
"sha": "6d6ac3889bf448e33a0ad96174bc5b0849c74ebe"
|
||||
},
|
||||
"homepage": "https://github.com/gemini-cli-extensions/dataproc"
|
||||
},
|
||||
@@ -1101,7 +1101,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/datarobot-oss/datarobot-agent-skills.git",
|
||||
"sha": "f4b3c29db60e1d735285a6f51328a69a2b500338"
|
||||
"sha": "0e28dc839859f523f4d8d418612cdadb7a4a3ce7"
|
||||
},
|
||||
"homepage": "https://datarobot.com"
|
||||
},
|
||||
@@ -1221,7 +1221,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/exa-labs/exa-mcp-server.git",
|
||||
"sha": "34a6b2aa877307639a1750ad6b1a1f74c66a34f5"
|
||||
"sha": "40d9990f48c55301535b0ea2d950176e6f115df3"
|
||||
},
|
||||
"homepage": "https://exa.ai/docs/reference/exa-mcp"
|
||||
},
|
||||
@@ -1245,7 +1245,7 @@
|
||||
"url": "https://github.com/expo/skills.git",
|
||||
"path": "plugins/expo",
|
||||
"ref": "main",
|
||||
"sha": "81ee6546b0745dc666ebacfc1f48e4bff74fbf44"
|
||||
"sha": "ad897fdf0c6593d0cc7523198aa752c6f62adeda"
|
||||
},
|
||||
"homepage": "https://github.com/expo/skills/blob/main/plugins/expo/README.md"
|
||||
},
|
||||
@@ -1428,7 +1428,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/huggingface/skills.git",
|
||||
"sha": "093e0bb81e568503fbb091d694fbcdb6273d7f44"
|
||||
"sha": "35e8c35a1ae5b462e0bb23d444d25569c3bb6700"
|
||||
},
|
||||
"homepage": "https://github.com/huggingface/skills.git"
|
||||
},
|
||||
@@ -1442,7 +1442,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/hunter-io/claude-plugin.git",
|
||||
"sha": "9929ccf4f228171398049633da7afd8f1b65646b"
|
||||
"sha": "0a03795dfe7258f46e702a2898bfc25aebbfcc58"
|
||||
},
|
||||
"homepage": "https://hunter.io"
|
||||
},
|
||||
@@ -1456,7 +1456,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/heygen-com/hyperframes.git",
|
||||
"sha": "64eaad7d69b3c11b9e95b912f2c0b5070994078b"
|
||||
"sha": "56859b618f45f646835c717a8a6dfaabbbda636d"
|
||||
},
|
||||
"homepage": "https://hyperframes.heygen.com"
|
||||
},
|
||||
@@ -1510,7 +1510,7 @@
|
||||
"source": "github",
|
||||
"repo": "jfrog/claude-plugin",
|
||||
"commit": "259c8e718266c16e99b4f30ae9b1ed0f9f00d98d",
|
||||
"sha": "ac0ea16e9e96c9837b26d3c7d6bdfecd41cb6149"
|
||||
"sha": "320a5585d6d9747668bd20e1c512c577d1e871d3"
|
||||
},
|
||||
"homepage": "https://jfrog.com"
|
||||
},
|
||||
@@ -1563,7 +1563,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/langfuse/claude-observability-plugin.git",
|
||||
"sha": "597af67d6c6b369f3e55db6cfa2ebe444f1af46c"
|
||||
"sha": "938df41639efcaa22790b1a216308b6ed626a8b7"
|
||||
},
|
||||
"homepage": "https://langfuse.com/integrations/other/claude-code"
|
||||
},
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"url": "https://github.com/awslabs/startups.git",
|
||||
"path": "migrate/plugins/migration-to-aws",
|
||||
"ref": "main",
|
||||
"sha": "ac1d625107f2d1905e448f4886e60c3cb264ea00"
|
||||
"sha": "e49c21bf8b4883a9646938c00091633dfb8f483f"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/startups"
|
||||
},
|
||||
@@ -1981,7 +1981,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/nvsecurity/nightvision-skills.git",
|
||||
"sha": "c7807ab1e2064890bfa6c96af17fc5b5c58eeab7"
|
||||
"sha": "67af610a1da439e10b1714d3896a2a02bf1ebd63"
|
||||
},
|
||||
"homepage": "https://github.com/nvsecurity/nightvision-skills"
|
||||
},
|
||||
@@ -2034,7 +2034,7 @@
|
||||
"url": "https://github.com/oracle-samples/oracle-aidp-samples.git",
|
||||
"path": "ai/claude-code-plugins/oracle-ai-data-platform-workbench-databricks-migrator",
|
||||
"ref": "main",
|
||||
"sha": "80a37379a3c503dc8099872f2ce6780e7232f275"
|
||||
"sha": "a88bcf3a9f9acca94663a727de42d8535e869486"
|
||||
},
|
||||
"homepage": "https://docs.oracle.com/en/cloud/paas/ai-data-platform/index.html"
|
||||
},
|
||||
@@ -2206,7 +2206,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/PostHog/ai-plugin.git",
|
||||
"sha": "39a8593ef773f643c34cfdb759c0c0d6f7311c93"
|
||||
"sha": "835f4f647fec8a8fbde8ea00cf9b2432a35d7e5b"
|
||||
},
|
||||
"homepage": "https://posthog.com/docs/model-context-protocol"
|
||||
},
|
||||
@@ -2310,7 +2310,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/qdrant/skills.git",
|
||||
"sha": "7a91f32069374463d22b0ae7836fd42f3f98b1f6"
|
||||
"sha": "0651740b38ed466ad12907bfb848e5f4b71b25e2"
|
||||
},
|
||||
"homepage": "https://skills.qdrant.tech"
|
||||
},
|
||||
@@ -2451,7 +2451,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/rilldata/agent-skills.git",
|
||||
"sha": "9bdc4efa38a9ad419104fc2d1bb3e89529202487"
|
||||
"sha": "c8c8738f44826150d52304cd4fb70cc3ecbdca2e"
|
||||
},
|
||||
"homepage": "https://docs.rilldata.com/developers/build/ai-configuration"
|
||||
},
|
||||
@@ -2584,7 +2584,7 @@
|
||||
"url": "https://github.com/SAP/open-ux-tools.git",
|
||||
"path": "packages/fiori-mcp-server",
|
||||
"ref": "main",
|
||||
"sha": "10d3bfcb04eea88c6b1a61f7f13479d95398a8db"
|
||||
"sha": "0cddd1a565895e5a12623b9e6aa19758cdbf80df"
|
||||
},
|
||||
"homepage": "https://github.com/SAP/open-ux-tools/tree/main/packages/fiori-mcp-server"
|
||||
},
|
||||
@@ -2683,7 +2683,7 @@
|
||||
"url": "https://github.com/getsentry/cli.git",
|
||||
"path": "plugins/sentry-cli",
|
||||
"ref": "main",
|
||||
"sha": "6acb9aa84a8e02d2cc4b029e05266427fdb79559"
|
||||
"sha": "20b469aa5a21acd9bad0650670a08dbe671f499b"
|
||||
},
|
||||
"homepage": "https://sentry.io"
|
||||
},
|
||||
@@ -2928,7 +2928,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/JetBrains/teamcity-cli.git",
|
||||
"sha": "25c5f8d3bada8b56244f84fb7a119c330a80d998"
|
||||
"sha": "42ce6a22b1a8167120adbfa8de8b79f36e698133"
|
||||
},
|
||||
"homepage": "https://www.jetbrains.com/teamcity/"
|
||||
},
|
||||
@@ -3039,7 +3039,7 @@
|
||||
"url": "https://github.com/UI5/plugins-coding-agents.git",
|
||||
"path": "plugins/ui5-modernization",
|
||||
"ref": "main",
|
||||
"sha": "1d4dedd56afcd1c3269c4d80f09e2ddb7f1bf5be"
|
||||
"sha": "d1e3a43fa80ef160cb42689b88d665e25a5a81a1"
|
||||
},
|
||||
"homepage": "https://github.com/UI5/plugins-coding-agents"
|
||||
},
|
||||
@@ -3073,7 +3073,7 @@
|
||||
"url": "https://github.com/val-town/plugins.git",
|
||||
"path": "plugin",
|
||||
"ref": "main",
|
||||
"sha": "e32973e454d92280c191f30a2b814f1adc953d14"
|
||||
"sha": "22594eb245d5b06714c99248d68c333169274b21"
|
||||
},
|
||||
"homepage": "https://val.town"
|
||||
},
|
||||
@@ -3151,7 +3151,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/wix/skills.git",
|
||||
"sha": "a50d4c068ee95aa8646137fc6b78d12985651c14"
|
||||
"sha": "1ea953a29525ce8ff07a3b5a3a107927804c3eba"
|
||||
},
|
||||
"homepage": "https://dev.wix.com/docs/wix-cli/guides/development/about-wix-skills"
|
||||
},
|
||||
@@ -3204,7 +3204,7 @@
|
||||
"url": "https://github.com/zapier/zapier-mcp.git",
|
||||
"path": "plugins/zapier",
|
||||
"ref": "main",
|
||||
"sha": "28ad6ea10009a88b9ea80748fb1524363a5d54a7"
|
||||
"sha": "469651fe7fdaa3dfc6a476ef5bad6c354773366a"
|
||||
},
|
||||
"homepage": "https://github.com/zapier/zapier-mcp/tree/main/plugins/zapier"
|
||||
},
|
||||
|
||||
153
.github/scripts/external-pr-scope.js
vendored
Normal file
153
.github/scripts/external-pr-scope.js
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
// Shared logic for letting a NON-MEMBER pull request stay open and be reviewed, scoped to
|
||||
// the contributor's own already-listed plugin repo. No maintained allowlist, no individuals.
|
||||
//
|
||||
// Trust model: we do NOT verify the submitter's identity. We trust the SOURCE REPO. A PR is
|
||||
// in scope only if it ADDS marketplace.json entries whose source.url is a repo that ALREADY
|
||||
// backs a live entry in this marketplace (derived from the base marketplace.json), pinned to
|
||||
// a commit in that repo. Because the repo is org-controlled and the SHA pins to a real commit
|
||||
// there, the shipped code is the org's code regardless of who opened the PR. Merge still
|
||||
// requires CI + a maintainer approval.
|
||||
//
|
||||
// Used by:
|
||||
// - close-external-prs.yml (skip the auto-close when in scope)
|
||||
// - external-pr-scope-guard.yml (required status check: fail a non-member PR that is out of scope)
|
||||
//
|
||||
// Security: evaluate() reads base + head marketplace.json as DATA via the API and parses them;
|
||||
// it never checks out or executes head code.
|
||||
|
||||
const MARKETPLACE = '.claude-plugin/marketplace.json';
|
||||
|
||||
function normalizeRepo(u) {
|
||||
return String(u || '').trim().toLowerCase()
|
||||
.replace(/^git\+/, '')
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\.git$/, '')
|
||||
.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function pluginsByName(json) {
|
||||
const map = {};
|
||||
for (const p of (json && json.plugins) || []) { if (p && p.name) map[p.name] = p; }
|
||||
return map;
|
||||
}
|
||||
|
||||
// Repos that already back a live entry, derived from the base marketplace.json.
|
||||
function liveReposOf(base) {
|
||||
const s = new Set();
|
||||
for (const name of Object.keys(base)) {
|
||||
const u = base[name] && base[name].source && base[name].source.url;
|
||||
if (!u) continue;
|
||||
const r = normalizeRepo(u);
|
||||
if (r.split('/').length >= 3) s.add(r); // host/org/repo
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Pure decision over an already-computed diff. Returns { ok, problems, added, removed, modified }.
|
||||
// before = plugins at the MERGE-BASE (what head forked from), after = plugins at HEAD,
|
||||
// liveRepos = repos already live on the current base branch. Diffing before->after (not
|
||||
// base-tip->head) isolates THIS PR's changes; a stale fork no longer shows main's later
|
||||
// additions as phantom removals.
|
||||
function analyze({ changedFiles, before, after, liveRepos }) {
|
||||
const problems = [];
|
||||
|
||||
const off = changedFiles.filter(n => n !== MARKETPLACE);
|
||||
if (off.length) problems.push(`changes files other than ${MARKETPLACE}: ${off.join(', ')}`);
|
||||
|
||||
const baseNames = new Set(Object.keys(before));
|
||||
const headNames = new Set(Object.keys(after));
|
||||
const removed = [...baseNames].filter(n => !headNames.has(n));
|
||||
const added = [...headNames].filter(n => !baseNames.has(n));
|
||||
const modified = [...headNames].filter(
|
||||
n => baseNames.has(n) && JSON.stringify(before[n]) !== JSON.stringify(after[n])
|
||||
);
|
||||
|
||||
if (removed.length) problems.push(`removes existing entr${removed.length > 1 ? 'ies' : 'y'}: ${removed.join(', ')}`);
|
||||
if (modified.length) problems.push(`modifies existing entr${modified.length > 1 ? 'ies' : 'y'}: ${modified.join(', ')}`);
|
||||
if (!off.length && !added.length && !removed.length && !modified.length) {
|
||||
problems.push('makes no in-scope change (expected additions to marketplace.json)');
|
||||
}
|
||||
|
||||
for (const name of added) {
|
||||
const u = after[name] && after[name].source && after[name].source.url;
|
||||
if (!u) { problems.push(`added "${name}" has no source.url to validate`); continue; }
|
||||
const r = normalizeRepo(u);
|
||||
if (r.split('/').length < 3) { problems.push(`added "${name}" source.url ${u} is not a valid repo URL`); continue; }
|
||||
if (!liveRepos.has(r)) {
|
||||
problems.push(`added "${name}" points at ${u}, a repo with no existing live plugin in this marketplace`);
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: problems.length === 0, problems, added, removed, modified, liveRepoCount: liveRepos.size };
|
||||
}
|
||||
|
||||
async function readPlugins(github, owner, repo, ref) {
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({ owner, repo, ref, path: MARKETPLACE });
|
||||
return pluginsByName(JSON.parse(Buffer.from(data.content, 'base64').toString('utf8')));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// API wrapper used by both workflows. Fetches the diff + base/head marketplace.json, delegates to analyze().
|
||||
async function evaluate({ github, context }) {
|
||||
const pr = context.payload.pull_request;
|
||||
const owner = context.repo.owner, repo = context.repo.repo;
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: pr.number, per_page: 100,
|
||||
});
|
||||
const changedFiles = files.map(f => f.filename);
|
||||
|
||||
// Diff THIS PR's changes (merge-base -> head), not base-tip -> head, so a fork that is
|
||||
// behind main doesn't show main's later additions as phantom removals.
|
||||
let mergeBaseSha = pr.base.sha;
|
||||
try {
|
||||
const cmp = await github.rest.repos.compareCommits({ owner, repo, base: pr.base.sha, head: pr.head.sha });
|
||||
if (cmp && cmp.data && cmp.data.merge_base_commit && cmp.data.merge_base_commit.sha) {
|
||||
mergeBaseSha = cmp.data.merge_base_commit.sha;
|
||||
}
|
||||
} catch (e) { /* fall back to base.sha */ }
|
||||
|
||||
const liveBase = await readPlugins(github, owner, repo, pr.base.sha); // current base branch (for "already live")
|
||||
const before = await readPlugins(github, owner, repo, mergeBaseSha); // what head forked from
|
||||
const after = await readPlugins(github, pr.head.repo.owner.login, pr.head.repo.name, pr.head.sha);
|
||||
if (liveBase === null || before === null || after === null) {
|
||||
return { ok: false, problems: ['could not read marketplace.json at base, merge-base, and/or head'], added: [], removed: [], modified: [] };
|
||||
}
|
||||
|
||||
return analyze({ changedFiles, before, after, liveRepos: liveReposOf(liveBase) });
|
||||
}
|
||||
|
||||
// Authors that are NOT subject to the external-contributor scope rules:
|
||||
// - the repo's own automation bot — its bump PRs legitimately MODIFY existing entries
|
||||
// (SHA bumps), which the additions-only external-contributor rule forbids; AND
|
||||
// - org members (write/admin).
|
||||
// Safe under pull_request_target: a fork PR cannot set its author to github-actions[bot]
|
||||
// (that login is only ever the org's own GITHUB_TOKEN workflow), and the member path is a
|
||||
// real permission lookup. Wrapped in try/catch because getCollaboratorPermissionLevel throws
|
||||
// for a non-collaborator/unknown user — without this, both callers would error the job rather
|
||||
// than fall through to scope evaluation.
|
||||
const EXEMPT_BOTS = new Set(['github-actions[bot]']);
|
||||
|
||||
async function isExemptAuthor({ github, context }) {
|
||||
const author = context.payload.pull_request.user.login;
|
||||
if (EXEMPT_BOTS.has(author)) {
|
||||
return { exempt: true, reason: `${author} is the trusted automation bot` };
|
||||
}
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner, repo: context.repo.repo, username: author,
|
||||
});
|
||||
if (['admin', 'write'].includes(data.permission)) {
|
||||
return { exempt: true, reason: `${author} is ${data.permission} (member)` };
|
||||
}
|
||||
} catch (e) {
|
||||
// not a collaborator / lookup failed → not exempt; fall through to scope evaluation
|
||||
}
|
||||
return { exempt: false };
|
||||
}
|
||||
|
||||
module.exports = { normalizeRepo, liveReposOf, analyze, readPlugins, evaluate, isExemptAuthor, MARKETPLACE };
|
||||
34
.github/workflows/close-external-prs.yml
vendored
34
.github/workflows/close-external-prs.yml
vendored
@@ -7,30 +7,46 @@ on:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-membership:
|
||||
if: vars.DISABLE_EXTERNAL_PR_CHECK != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if author has write access
|
||||
# pull_request_target: checks out the BASE repo (trusted), so the allowlist + shared
|
||||
# script below are this repo's versions, never the fork's.
|
||||
- uses: actions/checkout@v4
|
||||
- name: Close PR unless author is a member or the PR is an in-scope external contribution
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: author
|
||||
});
|
||||
const { evaluate, isExemptAuthor } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`);
|
||||
|
||||
if (['admin', 'write'].includes(data.permission)) {
|
||||
console.log(`${author} has ${data.permission} access, allowing PR`);
|
||||
// Members (write/admin) and the repo's own automation bot (bump SHA PRs) are never
|
||||
// auto-closed.
|
||||
const ex = await isExemptAuthor({ github, context });
|
||||
if (ex.exempt) {
|
||||
console.log(`${ex.reason} — allowing PR`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${author} has ${data.permission} access, closing PR`);
|
||||
// Non-member: allow the PR to stay open ONLY if it is an in-scope external
|
||||
// contribution — it adds marketplace.json entries whose source repo ALREADY backs
|
||||
// a live plugin here, and changes nothing else. (No maintained allowlist: the set
|
||||
// of allowed repos is derived from the live marketplace.) This grants only the
|
||||
// right to open a reviewable PR; the validate + scan checks and a maintainer
|
||||
// approval still gate the merge (the External PR Scope Guard is advisory signal,
|
||||
// not a required check).
|
||||
const result = await evaluate({ github, context });
|
||||
if (result.ok && result.added.length > 0) {
|
||||
console.log(`In-scope external contribution (adds: ${result.added.join(', ')}) — allowing PR.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Closing PR from ${author}: ${result.problems.join('; ') || 'out of scope'}`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
|
||||
54
.github/workflows/external-pr-scope-guard.yml
vendored
Normal file
54
.github/workflows/external-pr-scope-guard.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: External PR Scope Guard
|
||||
|
||||
# Advisory check that surfaces what a NON-MEMBER pull request may change.
|
||||
# Members (write/admin) and the repo's own automation bot (bump SHA PRs) 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).
|
||||
#
|
||||
# Do NOT add this job to branch protection as a required status check. The merge gate is the
|
||||
# `validate` + `scan` checks plus a maintainer approval; this guard is advisory signal for the
|
||||
# reviewer, not a hard gate. (Making it required would block the no-approval bump-merge path.)
|
||||
#
|
||||
# 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 { evaluate, isExemptAuthor } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`);
|
||||
|
||||
// Members (write/admin) and the repo's own automation bot (bump SHA PRs) are
|
||||
// unrestricted; only genuinely external contributions are scope-checked.
|
||||
const ex = await isExemptAuthor({ github, context });
|
||||
if (ex.exempt) {
|
||||
console.log(`${ex.reason} — scope guard not applicable.`);
|
||||
return;
|
||||
}
|
||||
|
||||
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.`);
|
||||
Reference in New Issue
Block a user