Skip to content

Commit 5897239

Browse files
izaitsevfbIvan
andauthored
Auto-dispatch AI CI Advisor on Dr.CI new failures (dark) (#8178)
**⚠️ Stacked on #8195** (the dispatch-extraction refactor) — this PR is based on `iz/advisor-refactor`, so the diff here is **only the new functionality**. Review/land #8195 first; this will retarget to `main` once it lands. Part 1 of the "auto-run CI Advisor on Dr.CI new failures" workstream, shipped **dark**: dispatch-only, feature-flagged, and prod-gated, so merging is inert until `DRCI_ADVISOR_AUTODISPATCH_ENABLED=true` **and** `VERCEL_ENV=production` **and** an `advisorConfig` entry are all present. ## What this does On every Dr.CI pass, after classification, auto-dispatch the AI advisor on each **NEW failure** (the `FAILED` bucket, cancelled jobs excluded) for enabled repos — reusing the shared dispatch lib from #8195 plus the existing advisor workflow + verdict store + PR/HUD verdict UI (which light up for free via the `dr_ci_${jobName}` signal-key convention). ## Reliable dedup (no dispatch storms) Dedup keys off a durable **dispatch marker**, not the verdict row (a verdict-write failure must not cause re-dispatch). - **`misc.ai_advisor_dispatches`** (new CH table): `SharedReplacingMergeTree`, `ORDER BY (owner, repo, head_sha, signal_key)`; the replacing `version` is deliberately **not** in `ORDER BY` so the pre/post rows collapse. No TTL — `head_sha` rotates on every push. - **Two-phase write**: a `dispatching` row is written **before** the dispatch and gates it (write-path down ⇒ don't dispatch); `dispatched` on success; a `failed` row supersedes for bounded retry (`retry_count` ≤ `MAX_DISPATCH_RETRIES`). - **Fails closed**: dedup read throws ⇒ dispatch nothing. ## Outage guard (per-repo) `AdvisorRepoConfig.maxNewFailures` (default 8): if a PR has more NEW failures than the max, **bail entirely** — a flood is almost always an outage, not a PR problem. ## Storm guard `request: { retries: 0 }` on the dispatch — GitHub's `workflow_dispatch` POST is non-idempotent and the default Octokit retry would create dup runs on a 5xx (the 2026-05-08 autorevert storm). ## Per-repo config (manual + auto) `lib/advisor/advisorConfig.ts` is the single source of truth: gates the auto loop, the manual endpoint (404 for non-enabled repos), and the manual button's visibility. Initial config: `pytorch/pytorch`. ## Minimal `drci.ts` change The auto-dispatch is a single try/catch after the `failures` map is built; the existing comment / early-return / check-run structure is untouched. ## ⚠️ Manual steps before enabling 1. Create the `misc.ai_advisor_dispatches` table (`schema.sql`). HUD's read/write users already hold DB-level access (no committed grants needed; `hud_user` SELECT verified). 2. Set `DRCI_ADVISOR_AUTODISPATCH_ENABLED=true` in the prod Vercel env. ## Test plan - `tsc --noEmit` clean; local lintrunner clean (incl. SQLFLUFF). - Unit tests: env/config gating, dedup skip states, retry bound, fail-closed read, pre-write-gate abort, dispatch-failure recording, outage bail (>max) + dispatch-all (=max), cancelled-job exclusion. (jest runs in CI on node 20.) ## Not in this PR - Verdicts inline in the Dr.CI comment (part 3); auto-suppress at merge (part 4); HUD PR-view classification reconcile (part 2). Co-authored-by: Ivan <izaitsevfb@meta.com>
1 parent 4bac884 commit 5897239

10 files changed

Lines changed: 776 additions & 44 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-- AI CI Advisor dispatch log.
2+
--
3+
-- Durable dedup + retry state for advisor workflow_dispatch runs, written
4+
-- synchronously by HUD (both the manual "AI Analyze" button and the automatic
5+
-- Dr.CI dispatch loop). Dedup is keyed on (owner, repo, head_sha, signal_key);
6+
-- head_sha rotates on every push, so a changed PR re-analyzes automatically and
7+
-- no TTL is needed.
8+
--
9+
-- Two writes per dispatch: 'dispatching' (pre-dispatch, also gates dispatch on
10+
-- write-path health -- if we cannot record the marker we must not dispatch)
11+
-- then 'dispatched' (post-dispatch). A 'failed' row supersedes the pre-dispatch
12+
-- row when the dispatch call throws, re-enabling retry up to retry_count = MAX.
13+
--
14+
-- ReplacingMergeTree(version) keeps the latest-version row per ORDER BY key. The
15+
-- version (a write timestamp) is deliberately NOT part of ORDER BY so the pre-
16+
-- and post-dispatch rows for a single dispatch collapse into one row (putting
17+
-- the timestamp in the sort key is the bug that stops misc.autorevert_events_v2
18+
-- rows from collapsing).
19+
CREATE TABLE misc.ai_advisor_dispatches
20+
(
21+
`owner` LowCardinality(String),
22+
`repo` LowCardinality(String),
23+
`head_sha` FixedString(40),
24+
`signal_key` String,
25+
`state` Enum8('dispatching' = 1, 'dispatched' = 2, 'failed' = 3),
26+
`retry_count` UInt8 DEFAULT 0,
27+
`pr_number` Int64,
28+
`job_name` String,
29+
`version` DateTime64(3),
30+
`_inserted_at` DateTime MATERIALIZED now()
31+
)
32+
ENGINE = SharedReplacingMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}', version)
33+
ORDER BY (owner, repo, head_sha, signal_key)
34+
SETTINGS index_granularity = 8192
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"params": {
3+
"owner": "String",
4+
"repo": "String",
5+
"headSha": "String",
6+
"signalKeys": "Array(String)"
7+
},
8+
"tests": []
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Latest AI advisor dispatch state + retry_count per signal_key for one PR head
2+
-- commit. Used to dedup auto-dispatch. argMax over the replacing version returns
3+
-- the latest row; reading (state, retry_count) as one tuple keeps them paired
4+
-- even if two writes ever share a version timestamp.
5+
SELECT
6+
signal_key,
7+
argMax((state, retry_count), version) AS sr
8+
FROM misc.ai_advisor_dispatches
9+
WHERE
10+
owner = {owner: String}
11+
AND repo = {repo: String}
12+
AND head_sha = {headSha: String}
13+
AND signal_key IN {signalKeys: Array(String)}
14+
GROUP BY signal_key

torchci/components/job/FilteredJobList.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isAdvisorEnabled } from "lib/advisor/advisorConfig";
12
import { fetcher } from "lib/GeneralUtils";
23
import { IssueData, JobAnnotation, JobData } from "lib/types";
34
import useScrollTo from "lib/useScrollTo";
@@ -27,15 +28,18 @@ function FailedJobInfo({
2728
return (
2829
<li key={job.id} id={job.id}>
2930
<JobSummary job={job} unstableIssues={unstableIssues} />
30-
{prNum > 0 && job.name && job.sha && (
31-
<AiAdvisorIndicator
32-
jobName={job.name}
33-
sha={job.sha}
34-
prNumber={prNum}
35-
conclusion={job.conclusion}
36-
workflowName={job.workflowName}
37-
/>
38-
)}
31+
{prNum > 0 &&
32+
job.name &&
33+
job.sha &&
34+
isAdvisorEnabled(repoOwner as string, repoName as string) && (
35+
<AiAdvisorIndicator
36+
jobName={job.name}
37+
sha={job.sha}
38+
prNumber={prNum}
39+
conclusion={job.conclusion}
40+
workflowName={job.workflowName}
41+
/>
42+
)}
3943
<div>
4044
<JobLinks job={job} />
4145
</div>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Per-repo configuration for the AI CI Advisor.
2+
//
3+
// Single source of truth shared by:
4+
// - the manual "AI Analyze" dispatch endpoint (pull/dispatch-advisor.ts),
5+
// - the automatic Dr.CI dispatch loop (lib/advisor/advisorDispatch.ts),
6+
// - the frontend (FilteredJobList.tsx) to gate the manual button.
7+
//
8+
// It is pure data with no server-only imports, so it can be imported from both
9+
// API routes and React components.
10+
11+
// If a PR has more than this many NEW failures, auto-dispatch bails for that PR
12+
// entirely (dispatches nothing) -- a flood of new failures is almost always an
13+
// outage rather than a PR problem, and we don't want to fan dozens of advisor
14+
// runs out per PR. Used as the fallback when a repo doesn't set its own.
15+
export const DEFAULT_MAX_NEW_FAILURES = 8;
16+
17+
export interface AdvisorRepoConfig {
18+
// The workflow_dispatch file (on the repo's default branch) that runs the
19+
// advisor analysis for this repo.
20+
workflowFile: string;
21+
// Auto-dispatch bails entirely if a PR has more than this many NEW failures
22+
// (outage guard). Falls back to DEFAULT_MAX_NEW_FAILURES when unset.
23+
maxNewFailures?: number;
24+
}
25+
26+
// Repos with the AI advisor enabled, keyed by "owner/repo".
27+
//
28+
// Adding a repo here enables BOTH the manual button and -- when the
29+
// DRCI_ADVISOR_AUTODISPATCH_ENABLED flag is on and VERCEL_ENV is production --
30+
// automatic dispatch on Dr.CI new failures. Enabling a repo is a deliberate,
31+
// reviewed action because every dispatch costs an LLM analysis.
32+
export const ADVISOR_REPOS: Record<string, AdvisorRepoConfig> = {
33+
"pytorch/pytorch": {
34+
workflowFile: "claude-autorevert-advisor.yml",
35+
maxNewFailures: 8,
36+
},
37+
};
38+
39+
export function getAdvisorRepoConfig(
40+
owner: string,
41+
repo: string
42+
): AdvisorRepoConfig | undefined {
43+
return ADVISOR_REPOS[`${owner}/${repo}`];
44+
}
45+
46+
export function isAdvisorEnabled(owner: string, repo: string): boolean {
47+
return getAdvisorRepoConfig(owner, repo) !== undefined;
48+
}
49+
50+
// The NEW-failure count above which auto-dispatch bails for a PR, per repo.
51+
export function getMaxNewFailures(owner: string, repo: string): number {
52+
return (
53+
getAdvisorRepoConfig(owner, repo)?.maxNewFailures ??
54+
DEFAULT_MAX_NEW_FAILURES
55+
);
56+
}

0 commit comments

Comments
 (0)