From f8aeedbdf2f5725b89d76d3b6c49eb52894b3092 Mon Sep 17 00:00:00 2001 From: Naman Trivedi Date: Thu, 13 Aug 2026 20:02:16 +0000 Subject: [PATCH] fix: skip orphaned check suites when checking GitHub CI `checkGitHubCI()` blocks landing with "GitHub CI is still running" whenever any GitHub Actions check suite has a status other than COMPLETED. GitHub occasionally creates check suites that stay stuck in QUEUED with zero check runs and are never dispatched, so they never complete. These orphaned suites block the commit queue indefinitely and force collaborators to land PRs manually. Skip a non-completed suite only when it has no check runs and was created more than 3 hours ago, since a legitimately queued suite gets its runs attached within seconds. Suites with active runs still block as before, and a fresh empty suite (within the window) is not skipped. Adds `createdAt` to the checkSuites GraphQL query to compute suite age. Refs: https://github.com/nodejs/node-core-utils/issues/1160 Signed-off-by: Naman Trivedi --- lib/pr_checker.js | 15 ++++++- lib/queries/PR.gql | 1 + .../github-ci/check-suite-orphaned.json | 39 +++++++++++++++++++ test/unit/pr_checker.test.js | 22 +++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/github-ci/check-suite-orphaned.json 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();