-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
ci: Extract test names for flaky test issues #20298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
3dd6c07
616cea7
0aeee92
59b43c4
02a15db
ceeb914
79ca3b4
dc939a7
9c86b21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,11 @@ | |
| import { timestampInSeconds } from '../../src/utils/time'; | ||
|
|
||
| describe('Session', () => { | ||
| // TEMPORARY: Guaranteed failure to verify issue creation in CI | ||
| it('TEMPORARY - should fail to test annotation extraction', () => { | ||
| expect(true).toBe(false); | ||
|
Check failure on line 9 in packages/core/test/lib/session.test.ts
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m: I think this should go away before merging right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes doing a last test run 😄 |
||
| }); | ||
|
nicohrubec marked this conversation as resolved.
Outdated
|
||
|
|
||
| it('initializes with the proper defaults', () => { | ||
| const newSession = makeSession(); | ||
| const session = newSession.toJSON(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * CI Failure Reporter script. | ||
| * | ||
| * Creates GitHub issues for tests that fail on the develop branch. | ||
| * For each failed job in the workflow run, it fetches check run annotations | ||
| * to identify individual failing tests, then creates one issue per failing | ||
| * test using the FLAKY_CI_FAILURE_TEMPLATE.md template. Existing open issues | ||
| * with matching titles are skipped to avoid duplicates. | ||
| * | ||
| * Intended to be called from a GitHub Actions workflow via actions/github-script: | ||
| * | ||
| * const { default: run } = await import( | ||
| * `${process.env.GITHUB_WORKSPACE}/scripts/report-ci-failures.mjs` | ||
| * ); | ||
| * await run({ github, context, core }); | ||
| */ | ||
|
|
||
| import { readFileSync } from 'node:fs'; | ||
|
|
||
| export default async function run({ github, context, core }) { | ||
| const { owner, repo } = context.repo; | ||
|
|
||
| // Fetch actual job details from the API to get descriptive names | ||
| const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { | ||
| owner, | ||
| repo, | ||
| run_id: context.runId, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| const failedJobs = jobs.filter(job => job.conclusion === 'failure' && !job.name.includes('(optional)')); | ||
|
|
||
| if (failedJobs.length === 0) { | ||
| core.info('No failed jobs found'); | ||
| return; | ||
| } | ||
|
|
||
| // Read and parse template | ||
| const template = readFileSync('.github/FLAKY_CI_FAILURE_TEMPLATE.md', 'utf8'); | ||
| const [, frontmatter, bodyTemplate] = template.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); | ||
|
|
||
| // Get existing open issues with Tests label | ||
| const existing = await github.paginate(github.rest.issues.listForRepo, { | ||
| owner, | ||
| repo, | ||
| state: 'open', | ||
| labels: 'Tests', | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| for (const job of failedJobs) { | ||
| const jobName = job.name; | ||
| const jobUrl = job.html_url; | ||
|
|
||
| // Fetch annotations from the check run to extract failed test names | ||
| let testNames = []; | ||
| try { | ||
| const annotations = await github.paginate(github.rest.checks.listAnnotations, { | ||
| owner, | ||
| repo, | ||
| check_run_id: job.id, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| const testAnnotations = annotations.filter(a => a.annotation_level === 'failure' && a.path !== '.github'); | ||
| testNames = [...new Set(testAnnotations.map(a => a.title || a.path))]; | ||
| } catch (e) { | ||
| core.info(`Could not fetch annotations for ${jobName}: ${e.message}`); | ||
| } | ||
|
|
||
| // If no test names found, fall back to one issue per job | ||
| if (testNames.length === 0) { | ||
| testNames = ['Unknown test']; | ||
| } | ||
|
|
||
| // Create one issue per failing test for proper deduplication | ||
| for (const testName of testNames) { | ||
| const vars = { | ||
| JOB_NAME: jobName, | ||
| RUN_LINK: jobUrl, | ||
| TEST_NAME: testName, | ||
| }; | ||
|
|
||
| let title = frontmatter.match(/title:\s*'(.*)'/)[1]; | ||
| let issueBody = bodyTemplate; | ||
| for (const [key, value] of Object.entries(vars)) { | ||
| const pattern = new RegExp(`\\{\\{\\s*env\\.${key}\\s*\\}\\}`, 'g'); | ||
| title = title.replace(pattern, value); | ||
| issueBody = issueBody.replace(pattern, value); | ||
|
nicohrubec marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const existingIssue = existing.find(i => i.title === title); | ||
|
|
||
| if (existingIssue) { | ||
| core.info(`Issue already exists for "${testName}" in ${jobName}: #${existingIssue.number}`); | ||
| continue; | ||
| } | ||
|
|
||
| const newIssue = await github.rest.issues.create({ | ||
| owner, | ||
| repo, | ||
| title, | ||
| body: issueBody.trim(), | ||
| labels: ['Tests'], | ||
| }); | ||
| core.info(`Created issue #${newIssue.data.number} for "${testName}" in ${jobName}`); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means issue titles could get quite long but I guess it's somewhat necessary when we need an automated way to set the title to something more specific than the job name. Easy to do manually, hard for a machine :D anyway, we can follow up on this. not a blocker for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking we could ask an LLM but that would break deduplication, i.e. we would need to put in a deterministic ruleset else this won't work properly. I don't wanna over engineer this, but we could definitely think about putting in some simple heuristics if we can think of anything that makes sense! I'll merge as is for now and maybe we can revisit later