ci(e2e): report Cypress retries in CI so flaky specs are visible - #3033
ci(e2e): report Cypress retries in CI so flaky specs are visible#3033fra-shipper wants to merge 2 commits into
Conversation
cypress.config.ts sets retries: 3, so a spec that fails and then passes on retry reports the job green with no trace anywhere a human will look (Chainlit#3023 was a real oauth_auth failure absorbed this way). Extend the after:spec handler, which previously discarded both of its arguments, to record tests where an attempt failed but the spec still passed, and write them to cypress/reports/retries.json per shard. Each e2e-tests.yaml matrix job now uploads that file as an artifact, and a new report-retries job downloads every shard's artifact, merges them, and writes a table to $GITHUB_STEP_SUMMARY. When a PR has retries, the job also posts or updates a best-effort PR comment; on forked PRs GITHUB_TOKEN is read-only, so the comment is skipped and the step summary remains the only, always-available output. Tested with cypress/support/retryReport.test.ts (node --test), which fails against the previous discard-everything behavior and passes against the new detection logic.
ci.yaml's e2e-tests: job called e2e-tests.yaml with no permissions
override, so it inherited ci.yaml's top-level `permissions: read-all`.
GitHub Actions permissions can only be downgraded through a
reusable-workflow call chain, never elevated, so report-retries'
own `permissions: {contents: read, pull-requests: write}` never
actually took effect: the PR-comment calls were always going to 403
and get swallowed by the existing core.warning(), even on same-repo
PRs. Add the matching permissions block to the caller job so the
grant can flow through.
Also narrow tsconfig.json's new exclude from `cypress/**/*.test.ts`
to the one file that needs it, so a future cypress *.test.ts file
doesn't silently lose type-checking.
There was a problem hiding this comment.
6 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tsconfig.json">
<violation number="1" location="tsconfig.json:9">
P2: By excluding cypress/support/retryReport.test.ts from the project tsconfig, the new test file is removed from all static type-checking. It only runs under node --test, which strips types without checking them, so type errors in the test (e.g. the loosely-typed asTests casts) can land in CI undetected. Keep the test type-checked instead of excluding it: give the test its own tsconfig that enables allowImportingTsExtensions (with a noEmit check), or include it with that flag set, so it stays covered without breaking the main compile.</violation>
</file>
<file name=".github/workflows/e2e-tests.yaml">
<violation number="1" location=".github/workflows/e2e-tests.yaml:124">
P2: When a later PR run has no retried tests, this early return leaves the previous marker comment untouched. Remove or update the existing marker comment on the zero-retry path instead of returning before comment reconciliation.</violation>
<violation number="2" location=".github/workflows/e2e-tests.yaml:155">
P3: The PR comment reports `retries.length` as "test attempt(s)", but each entry represents one retried test and can contain multiple retry attempts. Say "test(s)" or compute the total retry attempts.</violation>
<violation number="3" location=".github/workflows/e2e-tests.yaml:158">
P2: When a PR has more comments than the first API page, this call may not find the marker and creates a duplicate report comment. Paginate the comments before searching so the workflow maintains one comment.</violation>
</file>
<file name="cypress/support/retryReport.test.ts">
<violation number="1" location="cypress/support/retryReport.test.ts:9">
P2: This regression test is never executed: nothing in any CI workflow or npm/pnpm script runs `node --test cypress/support/retryReport.test.ts`, and tsconfig.json excludes the file. The retry-detection logic therefore has no automated coverage in CI, so a future change to collectRetriedTests can silently regress. Wire the test into CI (e.g. a workflow step or a `pnpm test` script entry) so it actually runs on every PR.</violation>
</file>
<file name="cypress.config.ts">
<violation number="1" location="cypress.config.ts:67">
P2: When `cypress open` runs a spec, `results` is undefined for `after:spec`; this dereference throws before `killChainlit()` runs. Guard the collection when results are unavailable.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }, | ||
| "include": ["cypress/**/*.ts", "cypress.config.ts"] | ||
| "include": ["cypress/**/*.ts", "cypress.config.ts"], | ||
| "exclude": ["cypress/support/retryReport.test.ts"] |
There was a problem hiding this comment.
P2: By excluding cypress/support/retryReport.test.ts from the project tsconfig, the new test file is removed from all static type-checking. It only runs under node --test, which strips types without checking them, so type errors in the test (e.g. the loosely-typed asTests casts) can land in CI undetected. Keep the test type-checked instead of excluding it: give the test its own tsconfig that enables allowImportingTsExtensions (with a noEmit check), or include it with that flag set, so it stays covered without breaking the main compile.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tsconfig.json, line 9:
<comment>By excluding cypress/support/retryReport.test.ts from the project tsconfig, the new test file is removed from all static type-checking. It only runs under node --test, which strips types without checking them, so type errors in the test (e.g. the loosely-typed asTests casts) can land in CI undetected. Keep the test type-checked instead of excluding it: give the test its own tsconfig that enables allowImportingTsExtensions (with a noEmit check), or include it with that flag set, so it stays covered without breaking the main compile.</comment>
<file context>
@@ -5,5 +5,6 @@
},
- "include": ["cypress/**/*.ts", "cypress.config.ts"]
+ "include": ["cypress/**/*.ts", "cypress.config.ts"],
+ "exclude": ["cypress/support/retryReport.test.ts"]
}
</file context>
| .addHeading('Cypress retries', 3) | ||
| .addRaw('No specs needed a retry in this run.') | ||
| .write(); | ||
| return; |
There was a problem hiding this comment.
P2: When a later PR run has no retried tests, this early return leaves the previous marker comment untouched. Remove or update the existing marker comment on the zero-retry path instead of returning before comment reconciliation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/e2e-tests.yaml, line 124:
<comment>When a later PR run has no retried tests, this early return leaves the previous marker comment untouched. Remove or update the existing marker comment on the zero-retry path instead of returning before comment reconciliation.</comment>
<file context>
@@ -73,3 +73,112 @@ jobs:
+ .addHeading('Cypress retries', 3)
+ .addRaw('No specs needed a retry in this run.')
+ .write();
+ return;
+ }
+
</file context>
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pullRequest.number | ||
| }); |
There was a problem hiding this comment.
P2: When a PR has more comments than the first API page, this call may not find the marker and creates a duplicate report comment. Paginate the comments before searching so the workflow maintains one comment.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/e2e-tests.yaml, line 158:
<comment>When a PR has more comments than the first API page, this call may not find the marker and creates a duplicate report comment. Paginate the comments before searching so the workflow maintains one comment.</comment>
<file context>
@@ -73,3 +73,112 @@ jobs:
+ const body = `${marker}\n**${retries.length} Cypress test attempt(s) were retried in this run.** See the job summary for details.`;
+
+ try {
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
</file context>
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pullRequest.number | |
| }); | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pullRequest.number | |
| } | |
| ); |
| const asTests = (tests: unknown) => | ||
| tests as CypressCommandLine.RunResult['tests']; | ||
|
|
||
| test('ignores a test that passed on its first attempt', () => { |
There was a problem hiding this comment.
P2: This regression test is never executed: nothing in any CI workflow or npm/pnpm script runs node --test cypress/support/retryReport.test.ts, and tsconfig.json excludes the file. The retry-detection logic therefore has no automated coverage in CI, so a future change to collectRetriedTests can silently regress. Wire the test into CI (e.g. a workflow step or a pnpm test script entry) so it actually runs on every PR.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cypress/support/retryReport.test.ts, line 9:
<comment>This regression test is never executed: nothing in any CI workflow or npm/pnpm script runs `node --test cypress/support/retryReport.test.ts`, and tsconfig.json excludes the file. The retry-detection logic therefore has no automated coverage in CI, so a future change to collectRetriedTests can silently regress. Wire the test into CI (e.g. a workflow step or a `pnpm test` script entry) so it actually runs on every PR.</comment>
<file context>
@@ -0,0 +1,63 @@
+const asTests = (tests: unknown) =>
+ tests as CypressCommandLine.RunResult['tests'];
+
+test('ignores a test that passed on its first attempt', () => {
+ const tests = asTests([
+ {
</file context>
|
|
||
| on('after:spec', async () => { | ||
| on('after:spec', async (spec, results) => { | ||
| retriedTests.push(...collectRetriedTests(spec.relative, results.tests)); |
There was a problem hiding this comment.
P2: When cypress open runs a spec, results is undefined for after:spec; this dereference throws before killChainlit() runs. Guard the collection when results are unavailable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cypress.config.ts, line 67:
<comment>When `cypress open` runs a spec, `results` is undefined for `after:spec`; this dereference throws before `killChainlit()` runs. Guard the collection when results are unavailable.</comment>
<file context>
@@ -41,12 +56,21 @@ export default defineConfig({
- on('after:spec', async () => {
+ on('after:spec', async (spec, results) => {
+ retriedTests.push(...collectRetriedTests(spec.relative, results.tests));
+ await mkdir(dirname(RETRY_REPORT_PATH), { recursive: true });
+ await writeFile(
</file context>
| retriedTests.push(...collectRetriedTests(spec.relative, results.tests)); | |
| if (results) | |
| retriedTests.push(...collectRetriedTests(spec.relative, results.tests)); |
| if (!pullRequest || isFork) return; | ||
|
|
||
| const marker = '<!-- cypress-retry-report -->'; | ||
| const body = `${marker}\n**${retries.length} Cypress test attempt(s) were retried in this run.** See the job summary for details.`; |
There was a problem hiding this comment.
P3: The PR comment reports retries.length as "test attempt(s)", but each entry represents one retried test and can contain multiple retry attempts. Say "test(s)" or compute the total retry attempts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/e2e-tests.yaml, line 155:
<comment>The PR comment reports `retries.length` as "test attempt(s)", but each entry represents one retried test and can contain multiple retry attempts. Say "test(s)" or compute the total retry attempts.</comment>
<file context>
@@ -73,3 +73,112 @@ jobs:
+ if (!pullRequest || isFork) return;
+
+ const marker = '<!-- cypress-retry-report -->';
+ const body = `${marker}\n**${retries.length} Cypress test attempt(s) were retried in this run.** See the job summary for details.`;
+
+ try {
</file context>
| const body = `${marker}\n**${retries.length} Cypress test attempt(s) were retried in this run.** See the job summary for details.`; | |
| const body = `${marker}\n**${retries.length} Cypress test(s) were retried in this run.** See the job summary for details.`; |
Fixes #3024.
Root cause
cypress.config.tssetsretries: 3, so a spec that fails and then passes on retry reports the job green with no trace anywhere a human will look. #3023 is a concrete example: a genuineoauth_authfailure onwindows-latest-3was absorbed by a retry, and finding it required pulling and grepping job logs by hand. The existingafter:spechandler discarded both of its arguments and only calledkillChainlit(), so this information was never captured.Fix
Implements Option B from the issue (self-contained, no Cypress Cloud dependency, works on forked PRs):
cypress/support/retryReport.tsaddscollectRetriedTests, a pure function that flags a test as retried when its overall state ispassedbut at least one attempt hasstate === 'failed'. Tests that exhaust every retry and still fail are excluded, since those are already visible via the job's exit status.cypress.config.ts'safter:spechandler now receives(spec, results), accumulates retried tests for the shard, and writes them tocypress/reports/retries.jsonafter every spec (so a killed job still leaves partial data)..github/workflows/e2e-tests.yamluploads that file as a per-shard artifact (cypress-retries-<os>-<containers>, same pattern as the existing screenshot upload), and a newreport-retriesjob downloads every shard's artifact, merges them, and writes a table to$GITHUB_STEP_SUMMARY.permissions: read-allis unchanged; thereport-retriesjob itself is grantedpull-requests: write, and.github/workflows/ci.yaml'se2e-tests:call site now also grantscontents: read/pull-requests: writeon the reusable-workflow job entry — a reusable workflow can only narrow permissions inherited from its caller, never widen them, so without this the grant insidee2e-tests.yamlwould have had no effect even on same-repo PRs. On forked PRsGITHUB_TOKENis read-only, so the comment step is skipped ahead of time based onpull_request.head.repo.full_nameand additionally wrapped in try/catch as a fallback; it never usespull_request_target. The step summary is always the primary, unconditional output.The unrelated dead
read-only-bannertestid assertion incypress/e2e/thread_resume/spec.cy.tsflagged in the issue as a separate item is left untouched, as the issue itself scopes it out.Testing
node --test cypress/support/retryReport.test.ts-> 3/3 pass (a test passing on first attempt is ignored, a test that fails then passes is reported with its attempt count, a test that fails every attempt is ignored). Verified this is a genuine regression test: temporarily revertedcollectRetriedTeststo a no-op returning[](matching the old discard-everythingafter:specbehavior) and reran — the retry-detection case failed with anAssertionErroras expected — then restored the real implementation and confirmed 3/3 pass again.npx prettier --checkon all touched files -> clean.npx eslintoncypress.config.ts,cypress/support/retryReport.ts,cypress/support/retryReport.test.ts-> clean.npx tsc --noEmit -p tsconfig.json-> no new errors from this change (only 4 pre-existingTS2428WeakMap errors from Cypress's vendored lodash types, confirmed present identically on a clean checkout viagit stash -u, and unrelated to this change — this tsconfig is also not wired into any CI script;pnpm type-checkonly recurses into thefrontend/libsworkspace packages).actionlinton the modified workflow files -> clean, exit 0.e2e-tests.yamlwas parsed withyaml.safe_loadto confirm it still parses correctly and the job list is['prepare', 'e2e-tests', 'report-retries']as intended.permissions:fix inci.yaml(a reusable-workflow call site can only narrow, never widen, the caller's inherited permissions) was verified by re-reading the affected files against GitHub's documented permission-inheritance semantics forworkflow_call, since that runtime behavior cannot be exercised locally or withactionlint(which validates syntax, not inheritance semantics).lint-staged, which runsformat:files,lint:fix, andactionlinton staged files) ran on every commit and made no further modifications, confirming the diff was already compliant.pnpm test:e2e(the full Cypress suite), since it requires the full Chainlit backend and a live browser; the pure retry-detection logic is covered by thenode:testregression test instead, and the artifact-upload/aggregation job will be exercised directly by this repo's owne2e-testsCI workflow on this PR.Summary by cubic
Makes Cypress retries visible in CI so flaky specs no longer fail silently (fixes #3024). Previously, a spec that failed on one attempt and passed on retry reported a green job with no trace anywhere a human would look — #3023 is a real
oauth_authfailure that was hidden this way.cypress/reports/retries.jsonper shard.report-retriesjob merges them into a$GITHUB_STEP_SUMMARYtable.pull-requests: writeat theci.yamlcall site so the grant passes through the reusable workflow (permissions can only be narrowed, never widened).Written for commit bb55b2f. Summary will update on new commits.