fix(github): dedupe re-run check-runs before CI classification - #3923
Conversation
reduceLiveCiAggregate had no deduplication-by-name step. GitHub's check-runs API (both the REST /check-runs endpoint and the GraphQL statusCheckRollup) can return multiple entries with the same `name` after a job is re-run (e.g. "Re-run failed jobs" following a flake): the stale run is left in the list rather than replaced. The classification loop unconditionally pushed every failing conclusion into failingDetails with no removal step, so a stale failing entry resolved ciState to "failed" even when the same-named check currently passes. This was reproduced against a real commit in this repo: a "Deploy UI preview version" check-run appears twice on 7d145f0 (conclusion "failure" from the original run, conclusion "skipped" from the re-run). That stale-failed ciState flows into planAgentMaintenanceActions as a terminal, immediate-close-driving signal for contributor PRs, bypassing the pending-CI wait entirely — so a contributor whose CI transiently failed and was legitimately re-run to green could still get auto-closed on the stale failure. Fix: add dedupeLatestCheckRunsByName, which collapses same-named check-runs to the one with the latest `started_at` before classification. It lives inside reduceLiveCiAggregate so both the REST path (fetchLiveCiAggregate) and the GraphQL path (fetchLiveCiAggregateViaGraphQl) get it from one place. `started_at` is used as the tiebreaker rather than check-run `id`, because the shared LiveCiCheckRun shape the GraphQL path populates has no `id` field (GraphQL check-run nodes don't expose one) — `started_at` is the one recency signal available on both REST and GraphQL, so the GraphQL query and mapping now also carry it through. When neither duplicate has a started_at (e.g. both still queued), array order is the fallback tiebreak, since GitHub does not document an ordering guarantee for /check-runs. Classic commit-statuses (the `statuses` array) are intentionally left alone: GitHub's Combined Status API is documented to already return one entry per unique context (the latest), so this duplicate-name failure mode does not apply there. Added regression tests covering: the stale-failure/fresh-pass case, the opposite case where the latest duplicate is the one that fails (proving the fix is recency-aware and not just duplicate-blind), an out-of-order duplicate list, the no-timestamp fallback, and the same scenarios through the GraphQL rollup path and its REST-equivalence suite.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-07 06:59:34 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 5 non-blocking
Concerns raised — review before merging
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3923 +/- ##
=======================================
Coverage 93.58% 93.58%
=======================================
Files 358 358
Lines 34342 34353 +11
Branches 12570 12574 +4
=======================================
+ Hits 32138 32149 +11
Misses 1580 1580
Partials 624 624
🚀 New features to boost your workflow:
|
Summary
reduceLiveCiAggregate(the shared classification the gate's live-CI aggregate uses for both the REST path and the flag-gated GraphQL path) had no deduplication-by-check-run-namestep. GitHub's check-runs API can return multiple entries with the samenamewhen a job is re-run (e.g. "Re-run failed jobs" after a flake) — the stale run is left in the list, not replaced. The classification loop pushed every failing conclusion intofailingDetailsunconditionally, so a stale failing duplicate resolvedciStateto"failed"even though the same-named check currently passes.7d145f032eb3b03b5ac5868aa3cecf3e002bb6e2has a "Deploy UI preview version" check-run appearing twice — one withconclusion: "failure"from the original run, one withconclusion: "skipped"from the re-run. That stale"failed"ciState flows intoplanAgentMaintenanceActionsas a terminal, immediate-close-driving signal for contributor PRs (it bypasses the pending-CI wait entirely), so a contributor whose CI transiently failed and was legitimately re-run to green could still get auto-closed on the stale failure.dedupeLatestCheckRunsByName, called at the top ofreduceLiveCiAggregate's check-run loop, which collapses same-named check-runs down to the one with the lateststarted_atbefore any classification happens. It lives in the one shared reducer, so bothfetchLiveCiAggregate(REST) andfetchLiveCiAggregateViaGraphQlget the fix from a single place.started_at, not check-runid. The sharedLiveCiCheckRunshape the GraphQL path populates has noid(GraphQLCheckRunnodes don't expose one), soidisn't usable in a fix that has to live in the shared reducer.started_atis available on both the REST payload and (once added to the GraphQL query/mapping, which this PR also does) the GraphQL side, so it's the one recency signal both paths can supply. When neither duplicate has astarted_at(e.g. both still queued), the fallback is array order — GitHub does not document an ordering guarantee for/check-runs, so this is a last-resort tiebreak, not an assumption the classifier depends on.statuses) have the same bug before deciding to leave them alone: GitHub's Combined Status API (/commits/{ref}/status) is documented to already return one entry per unique context, using the most recent status for that context — unlike/check-runs, which does not dedupe re-runs. I did not find any evidence in this codebase (existing comments, tests, or thestatusesaccumulation code) that this documented behavior doesn't hold, sostatusesis intentionally untouched.Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally (full unsharded run) —src/github/backfill.ts: 98.97% stmts / 97.2% branch / 96.81% funcs / 99.81% lines; every new line and branch indedupeLatestCheckRunsByNameand the GraphQLstarted_atplumbing shows non-zero hits incoverage/lcov.info(verified by grepping the exact added line numbers).npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderate— 0 vulnerabilitiestest/unit/backfill.test.tsand the matching GraphQL-path tests + REST-equivalence scenario intest/unit/graphql-status-rollup.test.ts.Ran the full local gate via
npm run test:ci, which chains all of the above plus migration/schema-drift/self-host-env-reference checks,docs:drift-check,command-reference:check, andui:test/ui:build— exit code 0.Safety
UI Evidencesection. — N/A, backend-only change, no visible UI/frontend/docs changes.Notes
started_atis now also read/mapped on the GraphQL rollup path (fetchLiveCiAggregateViaGraphQl's query and node mapping) purely so the shared dedup tiebreak has the same signal available on both paths — no other GraphQL behavior changes.