diff --git a/lib/pr_checker.js b/lib/pr_checker.js index 5e41eb8b..84116d3d 100644 --- a/lib/pr_checker.js +++ b/lib/pr_checker.js @@ -25,6 +25,10 @@ const WAIT_TIME_SINGLE_APPROVAL = 24 * 7; const GITHUB_SUCCESS_CONCLUSIONS = ['SUCCESS', 'NEUTRAL', 'SKIPPED']; const GITHUB_ACTIONS_APP = 'github-actions'; +// A GitHub Actions check suite with no runs that has not started after this +// long is considered orphaned (created but never dispatched by GitHub) and is +// ignored so it does not block landing. See nodejs/node-core-utils#1160. +const ORPHANED_CHECK_SUITE_TIMEOUT = MINUTE * 60 * 3; const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/; const FAST_TRACK_MIN_APPROVALS = 2; @@ -460,8 +464,17 @@ export default class PRChecker { const pendingJobs = []; // GitHub new Check API - for (const { status, conclusion, checkRuns } of checkSuites.nodes) { + for (const { status, conclusion, checkRuns, createdAt } of checkSuites.nodes) { if (status !== 'COMPLETED') { + // Skip orphaned check suites: no runs and never dispatched by GitHub. + // They will never complete, so they should not block landing. + const runCount = checkRuns?.nodes?.length ?? 0; + const age = Date.now() - new Date(createdAt).getTime(); + if (runCount === 0 && age > ORPHANED_CHECK_SUITE_TIMEOUT) { + cli.warn('Ignoring orphaned check suite with no runs ' + + `(status: ${status}, age: ${Math.round(age / MINUTE / 60)}h)`); + continue; + } pendingJobs.push({ status, conclusion }); continue; } diff --git a/lib/queries/PR.gql b/lib/queries/PR.gql index 10400af4..af1350f0 100644 --- a/lib/queries/PR.gql +++ b/lib/queries/PR.gql @@ -35,6 +35,7 @@ query PR($prid: Int!, $owner: String!, $repo: String!) { # https://api.github.com/apps/github-actions checkSuites(first: 100, filterBy: { appId: 15368 }) { nodes { + createdAt, conclusion, status, checkRuns(first: 40) { diff --git a/test/fixtures/github-ci/check-suite-orphaned.json b/test/fixtures/github-ci/check-suite-orphaned.json new file mode 100644 index 00000000..3db9ae8e --- /dev/null +++ b/test/fixtures/github-ci/check-suite-orphaned.json @@ -0,0 +1,39 @@ +[ + { + "commit": { + "committedDate": "2017-10-26T12:10:20Z", + "oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d", + "messageHeadline": "doc: add api description README", + "author": { + "login": "foo" + }, + "checkSuites": { + "nodes": [ + { + "status": "COMPLETED", + "conclusion": "SUCCESS", + "createdAt": "2024-01-01T00:00:00Z", + "checkRuns": { + "nodes": [ + { + "name": "test-linux", + "status": "COMPLETED", + "conclusion": "SUCCESS", + "detailsUrl": "https://example.com" + } + ] + } + }, + { + "status": "QUEUED", + "conclusion": null, + "createdAt": "2024-01-01T00:00:00Z", + "checkRuns": { + "nodes": [] + } + } + ] + } + } + } +] diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index a64c4855..4bcc701a 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -1803,6 +1803,28 @@ describe('PRChecker', () => { cli.assertCalledWith(expectedLogs); }); + it('should skip orphaned check suite with no runs older than the timeout', + async() => { + const cli = new TestCLI(); + + const expectedLogs = { + ok: [ + ['Last GitHub CI successful'] + ] + }; + + const commits = githubCI['check-suite-orphaned']; + const data = Object.assign({}, baseData, { commits }); + + const checker = new PRChecker(cli, data, {}, testArgv); + + const status = await checker.checkCI(); + assert(status); + // Ignore the warn channel: it reports the orphaned suite's age, which + // is computed from the current time and is therefore non-deterministic. + cli.assertCalledWith(expectedLogs, { ignore: ['warn'] }); + }); + it('should error if commit status failed', async() => { const cli = new TestCLI();