From d5e53bba157aabd635d61871fba4b5679ea62e6e Mon Sep 17 00:00:00 2001 From: Archkon <180910180+Archkon@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:28:16 +0800 Subject: [PATCH] fix(ci): ignore superseded GitHub Actions runs A commit can have multiple check suites for the same workflow and event when a branch is force-pushed away from and then back to the same SHA. Select the suite with the highest workflow run number and request only the latest check runs to avoid reporting superseded CI failures. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- lib/pr_checker.js | 35 +++++++--- lib/queries/PR.gql | 9 ++- .../fixtures/github-ci/both-apis-failure.json | 2 +- .../fixtures/github-ci/both-apis-success.json | 2 +- .../github-ci/check-suite-cancelled.json | 1 + .../github-ci/check-suite-failure.json | 1 + .../github-ci/check-suite-pending.json | 2 +- .../github-ci/check-suite-skipped.json | 1 + .../github-ci/check-suite-success.json | 1 + .../status-failure-check-suite-succeed.json | 2 +- .../status-succeed-check-suite-failure.json | 2 +- .../github-ci/success-dependabot-queued.json | 2 +- test/unit/graphql_queries.test.js | 3 +- test/unit/pr_checker.test.js | 69 +++++++++++++++++++ 14 files changed, 116 insertions(+), 16 deletions(-) diff --git a/lib/pr_checker.js b/lib/pr_checker.js index c4058f12..e28d1616 100644 --- a/lib/pr_checker.js +++ b/lib/pr_checker.js @@ -29,6 +29,30 @@ const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to const FAST_TRACK_MIN_APPROVALS = 2; const GIT_CONFIG_GUIDE_URL = 'https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork'; +function getLatestGitHubActionsCheckSuites(checkSuites) { + const latestByWorkflow = new Map(); + + for (const suite of checkSuites) { + const { app, workflowRun } = suite; + + if (app?.slug !== 'github-actions') { + continue; + } + + // Runs triggered by different events are independent and must not + // supersede one another. + const key = `${workflowRun.workflow.id}:${workflowRun.event}`; + const current = latestByWorkflow.get(key); + + if (!current || + workflowRun.runNumber > current.workflowRun.runNumber) { + latestByWorkflow.set(key, suite); + } + } + + return [...latestByWorkflow.values()]; +} + export const PR_CHECK_REASON_CODES = Object.freeze({ CANCELLED_GITHUB_CI: 'cancelled-github-ci', CLOSED: 'closed', @@ -459,14 +483,9 @@ export default class PRChecker { const pendingJobs = []; // GitHub new Check API - for (const { status, conclusion, app, checkRuns } of checkSuites.nodes) { - if (app.slug !== 'github-actions') { - // Ignore all non-github check suites, such as Dependabot and Codecov. - // They are expected to show up on PRs whose head branch is not on a - // fork and never complete. - continue; - } - + const latestCheckSuites = + getLatestGitHubActionsCheckSuites(checkSuites.nodes); + for (const { status, conclusion, app, checkRuns } of latestCheckSuites) { if (status !== 'COMPLETED') { pendingJobs.push({ app: app.slug, status, conclusion }); continue; diff --git a/lib/queries/PR.gql b/lib/queries/PR.gql index 824e5c5a..dabc79be 100644 --- a/lib/queries/PR.gql +++ b/lib/queries/PR.gql @@ -36,7 +36,14 @@ query PR($prid: Int!, $owner: String!, $repo: String!) { } conclusion, status, - checkRuns(first: 40) { + workflowRun { + event + runNumber + workflow { + id + } + } + checkRuns(first: 40, filterBy: { checkType: LATEST }) { nodes { name status diff --git a/test/fixtures/github-ci/both-apis-failure.json b/test/fixtures/github-ci/both-apis-failure.json index aa552cce..7b88192a 100644 --- a/test/fixtures/github-ci/both-apis-failure.json +++ b/test/fixtures/github-ci/both-apis-failure.json @@ -14,6 +14,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "FAILURE" } @@ -22,4 +23,3 @@ } } ] - diff --git a/test/fixtures/github-ci/both-apis-success.json b/test/fixtures/github-ci/both-apis-success.json index c58e7abc..b12fc607 100644 --- a/test/fixtures/github-ci/both-apis-success.json +++ b/test/fixtures/github-ci/both-apis-success.json @@ -14,6 +14,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "SUCCESS" } @@ -22,4 +23,3 @@ } } ] - diff --git a/test/fixtures/github-ci/check-suite-cancelled.json b/test/fixtures/github-ci/check-suite-cancelled.json index b42d16d6..db220db0 100644 --- a/test/fixtures/github-ci/check-suite-cancelled.json +++ b/test/fixtures/github-ci/check-suite-cancelled.json @@ -11,6 +11,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "CANCELLED", "checkRuns": { diff --git a/test/fixtures/github-ci/check-suite-failure.json b/test/fixtures/github-ci/check-suite-failure.json index d07172f0..519ef4a4 100644 --- a/test/fixtures/github-ci/check-suite-failure.json +++ b/test/fixtures/github-ci/check-suite-failure.json @@ -11,6 +11,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "FAILURE", "checkRuns": { diff --git a/test/fixtures/github-ci/check-suite-pending.json b/test/fixtures/github-ci/check-suite-pending.json index c09d11bf..2e53fadf 100644 --- a/test/fixtures/github-ci/check-suite-pending.json +++ b/test/fixtures/github-ci/check-suite-pending.json @@ -11,6 +11,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "IN_PROGRESS" } ] @@ -18,4 +19,3 @@ } } ] - diff --git a/test/fixtures/github-ci/check-suite-skipped.json b/test/fixtures/github-ci/check-suite-skipped.json index 6a2cb755..b7a021e7 100644 --- a/test/fixtures/github-ci/check-suite-skipped.json +++ b/test/fixtures/github-ci/check-suite-skipped.json @@ -11,6 +11,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "SKIPPED" } diff --git a/test/fixtures/github-ci/check-suite-success.json b/test/fixtures/github-ci/check-suite-success.json index 1dc5f395..4e65640c 100644 --- a/test/fixtures/github-ci/check-suite-success.json +++ b/test/fixtures/github-ci/check-suite-success.json @@ -11,6 +11,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "SUCCESS" } diff --git a/test/fixtures/github-ci/status-failure-check-suite-succeed.json b/test/fixtures/github-ci/status-failure-check-suite-succeed.json index 28faa58c..2f4805bc 100644 --- a/test/fixtures/github-ci/status-failure-check-suite-succeed.json +++ b/test/fixtures/github-ci/status-failure-check-suite-succeed.json @@ -14,6 +14,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "SUCCESS" } @@ -22,4 +23,3 @@ } } ] - diff --git a/test/fixtures/github-ci/status-succeed-check-suite-failure.json b/test/fixtures/github-ci/status-succeed-check-suite-failure.json index 6a336cd8..47d14b87 100644 --- a/test/fixtures/github-ci/status-succeed-check-suite-failure.json +++ b/test/fixtures/github-ci/status-succeed-check-suite-failure.json @@ -14,6 +14,7 @@ "nodes": [ { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "FAILURE" } @@ -22,4 +23,3 @@ } } ] - diff --git a/test/fixtures/github-ci/success-dependabot-queued.json b/test/fixtures/github-ci/success-dependabot-queued.json index 557452f5..1c6ddb9c 100644 --- a/test/fixtures/github-ci/success-dependabot-queued.json +++ b/test/fixtures/github-ci/success-dependabot-queued.json @@ -18,6 +18,7 @@ }, { "app": { "slug": "github-actions" }, + "workflowRun": { "event": "pull_request", "runNumber": 1, "workflow": { "id": "workflow-1" } }, "status": "COMPLETED", "conclusion": "SUCCESS" } @@ -26,4 +27,3 @@ } } ] - diff --git a/test/unit/graphql_queries.test.js b/test/unit/graphql_queries.test.js index 9b8d750e..c2fcfda7 100644 --- a/test/unit/graphql_queries.test.js +++ b/test/unit/graphql_queries.test.js @@ -17,7 +17,8 @@ describe('GraphQL queries', () => { assert.notStrictEqual(headCommitStart, -1); assert.notStrictEqual(headCommitEnd, -1); assert.match(headCommitQuery, /checkSuites\(first: 100\)/); - assert.match(headCommitQuery, /checkRuns\(first: 40\)/); + assert.match(headCommitQuery, + /checkRuns\(first: 40, filterBy: \{ checkType: LATEST \}\)/); assert.match(headCommitQuery, /status \{\s+state\s+\}/); assert.doesNotMatch(commitsQuery, /checkSuites/); assert.doesNotMatch(commitsQuery, /checkRuns/); diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index 109fc2cf..b3ce2d85 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -1825,6 +1825,45 @@ describe('PRChecker', () => { cli.assertCalledWith(expectedLogs); }); + it('should ignore a superseded Check suite', async() => { + const cli = new TestCLI(); + const commits = [{ + commit: { + checkSuites: { + nodes: [ + { + app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 2, + workflow: { id: 'workflow-1' } + }, + status: 'COMPLETED', + conclusion: 'SUCCESS' + }, + { + app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, + status: 'COMPLETED', + conclusion: 'FAILURE' + } + ] + } + } + }]; + const data = Object.assign({}, baseData, { commits }); + const checker = new PRChecker(cli, data, {}, testArgv); + + const status = await checker.checkCI(); + + assert(status); + cli.assertCalledWith({ ok: [['Last GitHub CI successful']] }); + }); + it('should error if commit status failed', async() => { const cli = new TestCLI(); @@ -2010,6 +2049,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'FAILURE', checkRuns: { @@ -2067,6 +2111,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'FAILURE', checkRuns: { @@ -2119,6 +2168,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'CANCELLED' // No checkRuns field @@ -2154,6 +2208,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'FAILURE', checkRuns: { nodes: [] } @@ -2189,6 +2248,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'FAILURE', checkRuns: { @@ -2231,6 +2295,11 @@ describe('PRChecker', () => { checkSuites: { nodes: [{ app: { slug: 'github-actions' }, + workflowRun: { + event: 'pull_request', + runNumber: 1, + workflow: { id: 'workflow-1' } + }, status: 'COMPLETED', conclusion: 'FAILURE', checkRuns: {