Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/pr_checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions lib/queries/PR.gql
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/github-ci/check-suite-orphaned.json
Original file line number Diff line number Diff line change
@@ -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": []
}
}
]
}
}
}
]
22 changes: 22 additions & 0 deletions test/unit/pr_checker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down