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:
@@ -39,6 +39,7 @@ jobs:
|
|||||||
# are evaluated.
|
# are evaluated.
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.draft == false &&
|
github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
||||||
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) == false
|
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
|
|||||||
11
.github/workflows/pr-title-validator.yml
vendored
11
.github/workflows/pr-title-validator.yml
vendored
@@ -94,6 +94,17 @@ jobs:
|
|||||||
const matcherPath = `${process.env.GITHUB_WORKSPACE}/scripts/release-notes/conventional-title.cjs`;
|
const matcherPath = `${process.env.GITHUB_WORKSPACE}/scripts/release-notes/conventional-title.cjs`;
|
||||||
|
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
|
|
||||||
|
// #4196: Dependabot PR titles (e.g. "chore(deps): bump ...")
|
||||||
|
// never carry `(#<issue>)` — there's no issue to link (its
|
||||||
|
// alerts live in the Security tab, not as repo issues). Exempt
|
||||||
|
// by author login, not title/branch shape, since that field is
|
||||||
|
// authenticated by GitHub and not forgeable.
|
||||||
|
if (pr.user && pr.user.login === 'dependabot[bot]') {
|
||||||
|
core.info('PR opened by dependabot[bot] — skipping title convention check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const title = pr.title || '';
|
const title = pr.title || '';
|
||||||
const warnOnly = process.env.WARN_ONLY === 'true';
|
const warnOnly = process.env.WARN_ONLY === 'true';
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/require-issue-link.yml
vendored
1
.github/workflows/require-issue-link.yml
vendored
@@ -103,6 +103,7 @@ jobs:
|
|||||||
PR_BODY: ${{ github.event.pull_request.body }}
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
|
PR_AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }}
|
||||||
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
|
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
|
||||||
CHANGED_FILES_TOTAL: ${{ github.event.pull_request.changed_files }}
|
CHANGED_FILES_TOTAL: ${{ github.event.pull_request.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const { runMain } = require('./lib/cli-exit.cjs');
|
|||||||
const ISSUE_LINK_REASON = Object.freeze({
|
const ISSUE_LINK_REASON = Object.freeze({
|
||||||
OK_CLOSING_KEYWORD: 'ok_closing_keyword',
|
OK_CLOSING_KEYWORD: 'ok_closing_keyword',
|
||||||
OK_BACKMERGE_EXEMPT: 'ok_backmerge_exempt',
|
OK_BACKMERGE_EXEMPT: 'ok_backmerge_exempt',
|
||||||
|
OK_DEPENDABOT_EXEMPT: 'ok_dependabot_exempt',
|
||||||
OK_FOLLOWUP_REFERENCE: 'ok_followup_reference',
|
OK_FOLLOWUP_REFERENCE: 'ok_followup_reference',
|
||||||
FAIL_NO_ISSUE_REFERENCE: 'fail_no_issue_reference',
|
FAIL_NO_ISSUE_REFERENCE: 'fail_no_issue_reference',
|
||||||
FAIL_REFERENCE_NEEDS_CLOSING: 'fail_reference_needs_closing',
|
FAIL_REFERENCE_NEEDS_CLOSING: 'fail_reference_needs_closing',
|
||||||
@@ -41,6 +42,14 @@ const ISSUE_LINK_REASON = Object.freeze({
|
|||||||
// ONLY when combined with `sameRepo === true` below (see header comment).
|
// ONLY when combined with `sameRepo === true` below (see header comment).
|
||||||
const BACKMERGE_BRANCH_PREFIX = 'chore/backmerge-main-to-next-';
|
const BACKMERGE_BRANCH_PREFIX = 'chore/backmerge-main-to-next-';
|
||||||
|
|
||||||
|
// #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, so there is nothing
|
||||||
|
// for it to reference. `pr.user.login` is authenticated by GitHub (not
|
||||||
|
// forgeable by a crafted title/branch), so this is safe without a sameRepo
|
||||||
|
// conjunct: no external actor can make GitHub report this login for a PR
|
||||||
|
// they opened.
|
||||||
|
const DEPENDABOT_LOGIN = 'dependabot[bot]';
|
||||||
|
|
||||||
// A follow-up-only PR (references an issue without closing it) is only
|
// A follow-up-only PR (references an issue without closing it) is only
|
||||||
// allowed to skip the closing keyword when every changed file is a test or
|
// allowed to skip the closing keyword when every changed file is a test or
|
||||||
// doc file — i.e. it cannot be the PR that actually implements the fix.
|
// doc file — i.e. it cannot be the PR that actually implements the fix.
|
||||||
@@ -122,7 +131,11 @@ function allPathsAreTestsOrDocs(changedFiles) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateIssueLink({ prBody, headRef, sameRepo, changedFiles, changedFilesTotal }) {
|
function evaluateIssueLink({ prBody, headRef, sameRepo, authorLogin, changedFiles, changedFilesTotal }) {
|
||||||
|
if (authorLogin === DEPENDABOT_LOGIN) {
|
||||||
|
return { ok: true, reason: ISSUE_LINK_REASON.OK_DEPENDABOT_EXEMPT };
|
||||||
|
}
|
||||||
|
|
||||||
if (hasClosingKeyword(prBody)) {
|
if (hasClosingKeyword(prBody)) {
|
||||||
return { ok: true, reason: ISSUE_LINK_REASON.OK_CLOSING_KEYWORD };
|
return { ok: true, reason: ISSUE_LINK_REASON.OK_CLOSING_KEYWORD };
|
||||||
}
|
}
|
||||||
@@ -159,6 +172,7 @@ function main() {
|
|||||||
prBody: process.env.PR_BODY || '',
|
prBody: process.env.PR_BODY || '',
|
||||||
headRef: process.env.HEAD_REF || '',
|
headRef: process.env.HEAD_REF || '',
|
||||||
sameRepo: process.env.SAME_REPO === 'true',
|
sameRepo: process.env.SAME_REPO === 'true',
|
||||||
|
authorLogin: process.env.PR_AUTHOR_LOGIN || '',
|
||||||
changedFiles,
|
changedFiles,
|
||||||
changedFilesTotal,
|
changedFilesTotal,
|
||||||
});
|
});
|
||||||
@@ -179,6 +193,7 @@ if (require.main === module) runMain(main);
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
ISSUE_LINK_REASON,
|
ISSUE_LINK_REASON,
|
||||||
BACKMERGE_BRANCH_PREFIX,
|
BACKMERGE_BRANCH_PREFIX,
|
||||||
|
DEPENDABOT_LOGIN,
|
||||||
EXEMPT_PATH_PREFIXES,
|
EXEMPT_PATH_PREFIXES,
|
||||||
EXCLUDED_ROOT_DOCS,
|
EXCLUDED_ROOT_DOCS,
|
||||||
CLOSING_KEYWORD_REGEX,
|
CLOSING_KEYWORD_REGEX,
|
||||||
|
|||||||
@@ -184,6 +184,26 @@ describe('evaluateIssueLink', () => {
|
|||||||
assert.strictEqual(result.reason, ISSUE_LINK_REASON.FAIL_NO_ISSUE_REFERENCE);
|
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
|
// 16. hasClosingKeyword corpus parity — expected values come from the
|
||||||
// shipped shell grep this regex replaces:
|
// shipped shell grep this regex replaces:
|
||||||
// grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'
|
// grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'
|
||||||
|
|||||||
Reference in New Issue
Block a user