ci(#4196): exempt dependabot[bot] from issue-link, title, and unsolicited-PR gates (#4203)

Dependabot has no mechanism to link a PR it opens to a repo issue -- its
alerts live in the Security tab, not as issues -- so require-issue-link,
pr-title-validator, and auto-close-unsolicited-prs all rejected its PRs
by design (confirmed live on #4193: auto-closed for "no pre-approved
issue", then flagged again by the title gate on reopen). Exempt by
authenticated author login (github.event.pull_request.user.login /
context.payload.pull_request.user.login), which GitHub attributes and a
crafted title or branch name cannot forge -- scoped narrowly to
dependabot[bot] only, no other author gets this treatment.

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-02 15:56:43 -04:00
committed by GitHub
parent 91ed46882a
commit 858bb89769
5 changed files with 49 additions and 1 deletions

View File

@@ -184,6 +184,26 @@ describe('evaluateIssueLink', () => {
assert.strictEqual(result.reason, ISSUE_LINK_REASON.FAIL_NO_ISSUE_REFERENCE);
});
// #4196: Dependabot has no mechanism to link a PR it opens to a repo
// issue (its alerts live in the Security tab, not as issues) — exempt by
// authenticated author login, with no reference and no source-file
// restriction, mirroring the backmerge exemption's structure above.
test('#4196: dependabot[bot] author is exempt with no reference at all, even on a source diff', () => {
const result = evaluateIssueLink(forkPr({
prBody: '', authorLogin: 'dependabot[bot]', changedFiles: ['src/init.cts'], changedFilesTotal: 1,
}));
assert.strictEqual(result.reason, ISSUE_LINK_REASON.OK_DEPENDABOT_EXEMPT);
assert.strictEqual(result.ok, true);
});
test('#4196 anti-forgery: an author login that merely CONTAINS "dependabot" is NOT exempt', () => {
const lookalikes = ['dependabot', 'Dependabot[bot]', 'not-dependabot[bot]', 'dependabot[bot] '];
for (const authorLogin of lookalikes) {
const result = evaluateIssueLink(forkPr({ prBody: '', authorLogin, changedFiles: ['src/init.cts'], changedFilesTotal: 1 }));
assert.strictEqual(result.reason, ISSUE_LINK_REASON.FAIL_NO_ISSUE_REFERENCE, `authorLogin: ${JSON.stringify(authorLogin)}`);
}
});
// 16. hasClosingKeyword corpus parity — expected values come from the
// shipped shell grep this regex replaces:
// grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'