Flake handling¶
How Bernstein detects, quarantines, and recovers from flaky tests.
TL;DR¶
- Two pieces of CI machinery: ctrf-io test reporter (per-PR markdown summary) and pytest-xflaky (nightly auto-quarantine PRs).
- A "flaky" test is one that fails at least twice AND passes at least twice across five consecutive runs of the unit suite under randomised ordering.
- Quarantined tests get
@pytest.mark.xfail(strict=False)- they still run, still emit XPASS when they pass, but stop blocking the merge gate. - Unquarantining requires three consecutive green runs after the decorator is removed.
Pipeline¶
1. Per-PR test summary (ctrf)¶
ctrf-io/github-test-reporter runs as a step in ci.yml::test after the JUnit producer step. It consumes the same junit.xml that mikepenz/action-junit-report already publishes and:
- Writes a markdown summary to the workflow Step Summary tab.
- Posts (or updates) a sticky comment on the triggering PR with the same content.
- Uploads the converted CTRF JSON as the
ctrf-reportworkflow artifact (7-day retention, sized to the xflaky look-back window).
The reporter highlights failed tests, slowest tests, and previously- flaky tests when present.
2. Nightly flake detection (pytest-xflaky)¶
.github/workflows/flake-quarantine.yml runs at 04:00 UTC every day and on workflow_dispatch. It:
- Installs the
devgroup pluspytest-randomly(the latter only in this ephemeral runner - it is intentionally not a global dev dep because it auto-activates on import and would shuffle every other CI job's test order). - Runs
pytest tests/unit --xflaky-collect --json-reportfive times: once with-p no:randomlyfor a deterministic baseline, then four times with--randomly-seed=lastto chain seeds. - Generates the xflaky reports (
--xflaky-report --xflaky-github-report) with the threshold--xflaky-min-failures 2 --xflaky-min-successes 2. - If any test was flagged, runs
pytest --xflaky-fixto rewrite the offending test files with@pytest.mark.xfail(strict=False). - Opens a PR via
peter-evans/create-pull-requestagainstmainwith the label setci, tests, flaky, bot.
The PR body explains the threshold, links to the artifacts, and gives operators an explicit checklist for investigation and unquarantining.
Operator workflow¶
Reviewing a quarantine PR¶
- Open the PR from branch
bot/flake-quarantine. - Read the affected test names. If they cluster around a single subsystem (network, async event loops, filesystem races), that is strong evidence of a shared root cause - file a bug to track the underlying defect.
- Pull the
xflaky-reportsartifact for per-run details. - Land the PR if the markers look reasonable. Close it if the run was infra noise - the next nightly run will redetect any real flake.
Investigating a quarantined test¶
- Pull the branch locally.
- Reproduce in isolation:
- Reproduce under randomised order:
- Reproduce under parallel execution (if parallel-safe):
- Common root causes for our codebase:
- Shared mutable global state across the agent registry.
- Hidden network calls (use
respxto make them deterministic). - Time-of-day assumptions (use
freezegun). - Filesystem races on
tmp_pathcleanup. - Event-loop bleed between async tests (check pytest-asyncio mode).
- Fix the root cause, remove the decorator, push.
Unquarantining policy¶
A test may be removed from quarantine when:
- The root cause is identified and fixed (preferred).
- The test passes three consecutive runs after the decorator is removed (operator runs
gh workflow run ci.yml --ref <branch>two extra times after the initial run).
If neither condition holds, the quarantine stays and the underlying defect is tracked as a regular bug.
Threshold tuning¶
The current thresholds (5 runs, min 2 failures, min 2 successes) trade off detection latency for false-positive rate:
- Lower
--xflaky-min-failureswould catch slower-flaking tests faster but file false-positive PRs more often. - More runs per night would tighten the signal at the cost of hosted- runner minutes - the current five-run budget is ~25 minutes per unit suite on a
ubuntu-latesthosted runner.
Both knobs live in .github/workflows/flake-quarantine.yml. Tune in that file rather than per-PR.
Out of scope¶
- Auto-unquarantine after a green-run streak. Easy to get wrong (XPASS noise vs. real recovery); tracked as a separate ticket.
- BuildPulse / flaky.io paid SaaS. Free-tier-only constraint.
- Test isolation primitives (
pytest-randomlyas a global dev dep). pytest-randomly stays local to the nightly job for the reason above.