close-draft-prs.yml (on pull_request_target after #760) cannot close fork draft PRs whose head branch name looks like a Git SHA — GitHub never dispatches pull_request_target for such branches, and a pull_request run from a fork gets a read-only token. So a draft PR on a SHA-named fork branch evades the auto-close. Add close-draft-prs-sweep.yml: a schedule (every 6h) + workflow_dispatch sweep running in base-repo context with pull-requests: write that paginates open PRs, filters to non-OWNER/MEMBER/COLLABORATOR drafts, and closes + comments them with the identical policy/message as the event-driven workflow. Re-fetches each candidate before mutating (TOCTOU guard), closes before commenting so enforcement is never gated on the explanatory comment, and core.setFailed on partial failures. The per-PR workflow remains the fast path; this is the safety net for the documented residual bypass. Extends tests/workflow-maintainer-skip.test.cjs with structural guards locking the triggers, write permission, maintainer carve-out, pagination, and message. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,4 +46,36 @@ describe('PR policy workflow maintainer carve-outs', () => {
|
||||
|
||||
assertMaintainerSkip(workflow);
|
||||
});
|
||||
|
||||
test('draft PR sweep enforces the same policy as the event-driven close', () => {
|
||||
const workflow = readWorkflow('.github/workflows/close-draft-prs-sweep.yml');
|
||||
|
||||
// Timer-driven in base-repo context, plus a manual dispatch for testing.
|
||||
// It must NOT be a fork-triggered event (no pull_request / pull_request_target trigger).
|
||||
assert.match(workflow, /schedule:/);
|
||||
assert.match(workflow, /cron:\s*'0 \*\/6 \* \* \*'/);
|
||||
assert.match(workflow, /workflow_dispatch:/);
|
||||
assert.doesNotMatch(workflow, /^\s*pull_request(_target)?:/m);
|
||||
|
||||
// Write-capable token (needed to close PRs from base context). Tolerant of
|
||||
// intervening blank lines or additional permission keys.
|
||||
assert.match(workflow, /permissions:\s+pull-requests:\s*write/);
|
||||
|
||||
// Identical maintainer carve-out to close-draft-prs.yml — a Set membership
|
||||
// test over author_association, negated (no github.event.pull_request in a
|
||||
// scheduled run).
|
||||
assert.match(workflow, /new Set\(\['OWNER', 'MEMBER', 'COLLABORATOR'\]\)/);
|
||||
assert.match(workflow, /!MAINTAINER_ASSOCIATIONS\.has\([^)]*\.author_association\)/);
|
||||
|
||||
// Paginates over open PRs and filters to drafts.
|
||||
assert.match(workflow, /github\.paginate\(github\.rest\.pulls\.list/);
|
||||
assert.match(workflow, /state:\s*'open'/);
|
||||
assert.match(workflow, /pr\.draft === true/);
|
||||
|
||||
// Same user-facing policy message as close-draft-prs.yml (locks the core
|
||||
// content so the sweep cannot silently drift to a weaker message).
|
||||
assert.match(workflow, /## Draft PRs are not accepted/);
|
||||
assert.match(workflow, /npm run test:coverage/);
|
||||
assert.match(workflow, /CONTRIBUTING\.md#pull-request-guidelines/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user