Skip to content

Commit 0b3954c

Browse files
nicohrubeccodex
andcommitted
fix(ci): Parallelize flaky report scans and log progress
Co-Authored-By: GPT-6 <codex@openai.com>
1 parent 24c6931 commit 0b3954c

1 file changed

Lines changed: 82 additions & 69 deletions

File tree

‎scripts/weekly-flaky-tests.mjs‎

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { normalizeJobName, normalizeTestName } from './report-ci-failures.mjs';
22

33
const LOOKBACK_DAYS = 7;
44
const MAX_TESTS = 10;
5+
const CONCURRENT_RUNS = 4;
56

67
function jobFamily(name) {
78
return normalizeJobName(
@@ -20,10 +21,11 @@ function markdownCell(value) {
2021
.replace(/[\r\n]+/g, ' ');
2122
}
2223

23-
export async function collectReport({ github, context, now = new Date() }) {
24+
export async function collectReport({ github, context, core, now = new Date() }) {
2425
const until = now.toISOString();
2526
const since = new Date(now.getTime() - LOOKBACK_DAYS * 24 * 60 * 60 * 1000).toISOString();
2627
const repo = context.repo;
28+
core.info(`Listing develop CI runs from ${since} to ${until}.`);
2729
const listedRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
2830
...repo,
2931
workflow_id: 'build.yml',
@@ -39,83 +41,94 @@ export async function collectReport({ github, context, now = new Date() }) {
3941
const tests = new Map();
4042
let failedJobsWithoutTests = 0;
4143
let failedJobs = 0;
44+
let completedRuns = 0;
45+
const pendingRuns = runs.filter(run => run.conclusion !== 'success' || run.run_attempt > 1);
46+
const runsToInspect = pendingRuns.length;
47+
core.info(
48+
`Found ${runs.length} runs; ${runsToInspect} need inspection, ${runs.length - runsToInspect} passed on the first attempt.`,
49+
);
4250

43-
for (const run of runs) {
44-
if (run.conclusion === 'success' && run.run_attempt === 1) {
45-
continue;
46-
}
51+
async function worker() {
52+
while (pendingRuns.length > 0) {
53+
const run = pendingRuns.shift();
54+
core.info(`Run ${run.id}: fetching jobs across ${run.run_attempt} attempt(s).`);
4755

48-
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
49-
...repo,
50-
run_id: run.id,
51-
filter: 'all',
52-
per_page: 100,
53-
});
54-
for (const job of jobs) {
55-
if (job.conclusion !== 'failure' || job.name.includes('(optional)')) {
56-
continue;
57-
}
58-
failedJobs++;
56+
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
57+
...repo,
58+
run_id: run.id,
59+
filter: 'all',
60+
per_page: 100,
61+
});
62+
const jobsToInspect = jobs.filter(job => job.conclusion === 'failure' && !job.name.includes('(optional)'));
63+
core.info(`Run ${run.id}: ${jobs.length} job attempts, ${jobsToInspect.length} failures to inspect.`);
64+
for (const job of jobsToInspect) {
65+
failedJobs++;
5966

60-
let annotations;
61-
try {
62-
annotations = await github.paginate(github.rest.checks.listAnnotations, {
63-
...repo,
64-
check_run_id: Number(job.check_run_url.split('/').pop()),
65-
per_page: 100,
66-
});
67-
} catch (error) {
68-
if (error.status !== 404 && error.status !== 410) {
69-
throw error;
67+
let annotations;
68+
try {
69+
annotations = await github.paginate(github.rest.checks.listAnnotations, {
70+
...repo,
71+
check_run_id: Number(job.check_run_url.split('/').pop()),
72+
per_page: 100,
73+
});
74+
} catch (error) {
75+
if (error.status !== 404 && error.status !== 410) {
76+
throw error;
77+
}
78+
warnings.push(`Annotations unavailable for job ${job.id} in run ${run.id}.`);
79+
continue;
7080
}
71-
warnings.push(`Annotations unavailable for job ${job.id} in run ${run.id}.`);
72-
continue;
73-
}
7481

75-
const failures = annotations.filter(
76-
annotation =>
77-
annotation.annotation_level === 'failure' &&
78-
annotation.title &&
79-
/(?:^|\/)(?:tests?|__tests__|suites)\/|(?:^|[/.])(?:test|spec)\.[cm]?[jt]sx?$/i.test(annotation.path),
80-
);
81-
if (failures.length === 0) {
82-
failedJobsWithoutTests++;
83-
}
84-
85-
// Match the exact matrix job, not its normalized family, when checking recovery.
86-
const recovered = jobs.some(
87-
later =>
88-
later.name === job.name &&
89-
later.head_sha === job.head_sha &&
90-
later.run_attempt > job.run_attempt &&
91-
later.conclusion === 'success',
92-
);
93-
for (const annotation of failures) {
94-
const family = jobFamily(job.name);
95-
const name = normalizeTestName(annotation.title);
96-
const key = JSON.stringify([family, annotation.path, name]);
97-
let test = tests.get(key);
98-
if (!test) {
99-
test = {
100-
family,
101-
path: annotation.path,
102-
name,
103-
runs: new Set(),
104-
recoveredRuns: new Set(),
105-
examples: new Map(),
106-
};
107-
tests.set(key, test);
108-
}
109-
test.runs.add(run.id);
110-
if (recovered) {
111-
test.recoveredRuns.add(run.id);
82+
const failures = annotations.filter(
83+
annotation =>
84+
annotation.annotation_level === 'failure' &&
85+
annotation.title &&
86+
/(?:^|\/)(?:tests?|__tests__|suites)\/|(?:^|[/.])(?:test|spec)\.[cm]?[jt]sx?$/i.test(annotation.path),
87+
);
88+
if (failures.length === 0) {
89+
failedJobsWithoutTests++;
11290
}
113-
if (test.examples.size < 3 && !test.examples.has(run.id)) {
114-
test.examples.set(run.id, job.html_url);
91+
92+
// Match the exact matrix job, not its normalized family, when checking recovery.
93+
const recovered = jobs.some(
94+
later =>
95+
later.name === job.name &&
96+
later.head_sha === job.head_sha &&
97+
later.run_attempt > job.run_attempt &&
98+
later.conclusion === 'success',
99+
);
100+
for (const annotation of failures) {
101+
const family = jobFamily(job.name);
102+
const name = normalizeTestName(annotation.title);
103+
const key = JSON.stringify([family, annotation.path, name]);
104+
let test = tests.get(key);
105+
if (!test) {
106+
test = {
107+
family,
108+
path: annotation.path,
109+
name,
110+
runs: new Set(),
111+
recoveredRuns: new Set(),
112+
examples: new Map(),
113+
};
114+
tests.set(key, test);
115+
}
116+
test.runs.add(run.id);
117+
if (recovered) {
118+
test.recoveredRuns.add(run.id);
119+
}
120+
if (test.examples.size < 3 && !test.examples.has(run.id)) {
121+
test.examples.set(run.id, job.html_url);
122+
}
115123
}
116124
}
125+
completedRuns++;
126+
core.info(
127+
`Completed ${completedRuns}/${runsToInspect} runs; ${failedJobs} failed jobs inspected, ${tests.size} failing tests found.`,
128+
);
117129
}
118130
}
131+
await Promise.all(Array.from({ length: CONCURRENT_RUNS }, () => worker()));
119132

120133
const ranked = [...tests.values()].sort(
121134
(a, b) =>
@@ -187,7 +200,7 @@ export function renderReport(report) {
187200
}
188201

189202
export default async function run({ github, context, core }) {
190-
const report = await collectReport({ github, context });
203+
const report = await collectReport({ github, context, core });
191204
for (const warning of report.warnings) {
192205
core.warning(warning);
193206
}

0 commit comments

Comments
 (0)