Skip to content

Commit 79ca3b4

Browse files
committed
extract script
1 parent ceeb914 commit 79ca3b4

2 files changed

Lines changed: 116 additions & 89 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,101 +1161,19 @@ jobs:
11611161
if: github.ref == 'refs/heads/develop' && contains(needs.*.result, 'failure')
11621162
uses: actions/checkout@v6
11631163
with:
1164-
sparse-checkout: .github
1164+
sparse-checkout: |
1165+
.github
1166+
scripts
11651167
11661168
- name: Create issues for failed jobs
11671169
if: github.ref == 'refs/heads/develop' && contains(needs.*.result, 'failure')
11681170
uses: actions/github-script@v7
11691171
with:
11701172
script: |
1171-
const fs = require('fs');
1172-
1173-
// Fetch actual job details from the API to get descriptive names
1174-
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
1175-
owner: context.repo.owner,
1176-
repo: context.repo.repo,
1177-
run_id: context.runId,
1178-
per_page: 100
1179-
});
1180-
1181-
const failedJobs = jobs.filter(job => job.conclusion === 'failure' && !job.name.includes('(optional)'));
1182-
1183-
if (failedJobs.length === 0) {
1184-
console.log('No failed jobs found');
1185-
return;
1186-
}
1187-
1188-
// Read and parse template
1189-
const template = fs.readFileSync('.github/FLAKY_CI_FAILURE_TEMPLATE.md', 'utf8');
1190-
const [, frontmatter, bodyTemplate] = template.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1191-
1192-
// Get existing open issues with Tests label
1193-
const existing = await github.paginate(github.rest.issues.listForRepo, {
1194-
owner: context.repo.owner,
1195-
repo: context.repo.repo,
1196-
state: 'open',
1197-
labels: 'Tests',
1198-
per_page: 100
1199-
});
1200-
1201-
for (const job of failedJobs) {
1202-
const jobName = job.name;
1203-
const jobUrl = job.html_url;
1204-
1205-
// Fetch annotations from the check run to extract failed test names
1206-
let testNames = [];
1207-
try {
1208-
const annotations = await github.paginate(github.rest.checks.listAnnotations, {
1209-
owner: context.repo.owner,
1210-
repo: context.repo.repo,
1211-
check_run_id: job.id,
1212-
per_page: 100
1213-
});
1214-
1215-
const testAnnotations = annotations.filter(a => a.annotation_level === 'failure' && a.path !== '.github');
1216-
testNames = [...new Set(testAnnotations.map(a => a.title || a.path))];
1217-
} catch (e) {
1218-
console.log(`Could not fetch annotations for ${jobName}: ${e.message}`);
1219-
}
1220-
1221-
// If no test names found, fall back to one issue per job
1222-
if (testNames.length === 0) {
1223-
testNames = ['Unknown test'];
1224-
}
1225-
1226-
// Create one issue per failing test for proper deduplication
1227-
for (const testName of testNames) {
1228-
const vars = {
1229-
'JOB_NAME': jobName,
1230-
'RUN_LINK': jobUrl,
1231-
'TEST_NAME': testName
1232-
};
1233-
1234-
let title = frontmatter.match(/title:\s*'(.*)'/)[1];
1235-
let issueBody = bodyTemplate;
1236-
for (const [key, value] of Object.entries(vars)) {
1237-
const pattern = new RegExp(`\\{\\{\\s*env\\.${key}\\s*\\}\\}`, 'g');
1238-
title = title.replace(pattern, value);
1239-
issueBody = issueBody.replace(pattern, value);
1240-
}
1241-
1242-
const existingIssue = existing.find(i => i.title === title);
1243-
1244-
if (existingIssue) {
1245-
console.log(`Issue already exists for "${testName}" in ${jobName}: #${existingIssue.number}`);
1246-
continue;
1247-
}
1248-
1249-
const newIssue = await github.rest.issues.create({
1250-
owner: context.repo.owner,
1251-
repo: context.repo.repo,
1252-
title: title,
1253-
body: issueBody.trim(),
1254-
labels: ['Tests']
1255-
});
1256-
console.log(`Created issue #${newIssue.data.number} for "${testName}" in ${jobName}`);
1257-
}
1258-
}
1173+
const { default: run } = await import(
1174+
`${process.env.GITHUB_WORKSPACE}/scripts/report-ci-failures.mjs`
1175+
);
1176+
await run({ github, context, core });
12591177
12601178
- name: Check for failures
12611179
if: cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')

scripts/report-ci-failures.mjs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* CI Failure Reporter script.
3+
*
4+
* Creates GitHub issues for tests that fail on the develop branch.
5+
* For each failed job in the workflow run, it fetches check run annotations
6+
* to identify individual failing tests, then creates one issue per failing
7+
* test using the FLAKY_CI_FAILURE_TEMPLATE.md template. Existing open issues
8+
* with matching titles are skipped to avoid duplicates.
9+
*
10+
* Intended to be called from a GitHub Actions workflow via actions/github-script:
11+
*
12+
* const { default: run } = await import(
13+
* `${process.env.GITHUB_WORKSPACE}/scripts/report-ci-failures.mjs`
14+
* );
15+
* await run({ github, context, core });
16+
*/
17+
18+
import { readFileSync } from 'node:fs';
19+
20+
export default async function run({ github, context, core }) {
21+
const { owner, repo } = context.repo;
22+
23+
// Fetch actual job details from the API to get descriptive names
24+
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
25+
owner,
26+
repo,
27+
run_id: context.runId,
28+
per_page: 100,
29+
});
30+
31+
const failedJobs = jobs.filter(job => job.conclusion === 'failure' && !job.name.includes('(optional)'));
32+
33+
if (failedJobs.length === 0) {
34+
core.info('No failed jobs found');
35+
return;
36+
}
37+
38+
// Read and parse template
39+
const template = readFileSync('.github/FLAKY_CI_FAILURE_TEMPLATE.md', 'utf8');
40+
const [, frontmatter, bodyTemplate] = template.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
41+
42+
// Get existing open issues with Tests label
43+
const existing = await github.paginate(github.rest.issues.listForRepo, {
44+
owner,
45+
repo,
46+
state: 'open',
47+
labels: 'Tests',
48+
per_page: 100,
49+
});
50+
51+
for (const job of failedJobs) {
52+
const jobName = job.name;
53+
const jobUrl = job.html_url;
54+
55+
// Fetch annotations from the check run to extract failed test names
56+
let testNames = [];
57+
try {
58+
const annotations = await github.paginate(github.rest.checks.listAnnotations, {
59+
owner,
60+
repo,
61+
check_run_id: job.id,
62+
per_page: 100,
63+
});
64+
65+
const testAnnotations = annotations.filter(a => a.annotation_level === 'failure' && a.path !== '.github');
66+
testNames = [...new Set(testAnnotations.map(a => a.title || a.path))];
67+
} catch (e) {
68+
core.info(`Could not fetch annotations for ${jobName}: ${e.message}`);
69+
}
70+
71+
// If no test names found, fall back to one issue per job
72+
if (testNames.length === 0) {
73+
testNames = ['Unknown test'];
74+
}
75+
76+
// Create one issue per failing test for proper deduplication
77+
for (const testName of testNames) {
78+
const vars = {
79+
JOB_NAME: jobName,
80+
RUN_LINK: jobUrl,
81+
TEST_NAME: testName,
82+
};
83+
84+
let title = frontmatter.match(/title:\s*'(.*)'/)[1];
85+
let issueBody = bodyTemplate;
86+
for (const [key, value] of Object.entries(vars)) {
87+
const pattern = new RegExp(`\\{\\{\\s*env\\.${key}\\s*\\}\\}`, 'g');
88+
title = title.replace(pattern, value);
89+
issueBody = issueBody.replace(pattern, value);
90+
}
91+
92+
const existingIssue = existing.find(i => i.title === title);
93+
94+
if (existingIssue) {
95+
core.info(`Issue already exists for "${testName}" in ${jobName}: #${existingIssue.number}`);
96+
continue;
97+
}
98+
99+
const newIssue = await github.rest.issues.create({
100+
owner,
101+
repo,
102+
title,
103+
body: issueBody.trim(),
104+
labels: ['Tests'],
105+
});
106+
core.info(`Created issue #${newIssue.data.number} for "${testName}" in ${jobName}`);
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)