Bare `pull_request` hands fork PRs a read-only GITHUB_TOKEN, so the close/comment API calls 403 and a first-time/external contributor's draft PR survives — bypassing the auto-close for exactly the population the job targets. Switch to `pull_request_target`, which runs in the base-repo context with a write-capable token even for fork PRs. Safe because the job never checks out or executes PR-supplied code; it only reads event metadata and calls the GitHub API. The minimal `permissions: pull-requests: write` block still constrains the token. Add a regression guard in tests/workflow-maintainer-skip.test.cjs asserting the workflow triggers on pull_request_target and not bare pull_request. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
2.0 KiB
JavaScript
50 lines
2.0 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// These workflow files are deployed policy; the tests lock the maintainer
|
|
// carve-out so future edits do not accidentally re-enable enforcement.
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const MAINTAINER_SKIP_EXPR = 'contains(fromJSON(\'["OWNER","MEMBER","COLLABORATOR"]\'), github.event.pull_request.author_association) == false';
|
|
|
|
function readWorkflow(relativePath) {
|
|
return fs.readFileSync(path.join(process.cwd(), relativePath), 'utf8');
|
|
}
|
|
|
|
function assertMaintainerSkip(source) {
|
|
assert.ok(
|
|
source.includes(MAINTAINER_SKIP_EXPR),
|
|
`Expected workflow to include maintainer skip expression: ${MAINTAINER_SKIP_EXPR}`
|
|
);
|
|
}
|
|
|
|
describe('PR policy workflow maintainer carve-outs', () => {
|
|
test('draft PR auto-close does not run for maintainer-authored PRs', () => {
|
|
const workflow = readWorkflow('.github/workflows/close-draft-prs.yml');
|
|
|
|
assert.match(workflow, /github\.event\.pull_request\.draft == true/);
|
|
assertMaintainerSkip(workflow);
|
|
});
|
|
|
|
test('draft PR auto-close triggers on pull_request_target so fork PRs cannot bypass it', () => {
|
|
const workflow = readWorkflow('.github/workflows/close-draft-prs.yml');
|
|
|
|
// A bare `pull_request` trigger hands fork PRs (how first-time/external
|
|
// contributors contribute) a read-only GITHUB_TOKEN, so the close/comment
|
|
// API calls 403 and the draft PR survives — bypassing the auto-close.
|
|
// `pull_request_target` runs in the base-repo context with a write-capable
|
|
// token. Guard against a regression back to the bypassable trigger.
|
|
assert.match(workflow, /^\s*pull_request_target:/m);
|
|
assert.doesNotMatch(workflow, /^\s*pull_request:\s*$/m);
|
|
});
|
|
|
|
test('PR target validator does not run for maintainer-authored PRs', () => {
|
|
const workflow = readWorkflow('.github/workflows/pr-target-validator.yml');
|
|
|
|
assertMaintainerSkip(workflow);
|
|
});
|
|
});
|