-
Notifications
You must be signed in to change notification settings - Fork 144
Add an AI_NOT_RELATED bucket for advisor-cleared failures #8671
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
Open
izaitsevfb
wants to merge
7
commits into
main
Choose a base branch
from
gh/izaitsevfb/1/head
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
3ee551c
Update (base update)
c6a07cd
Update
2154254
Read the advisor verdicts once, and resolve them the same way for bot…
fc90279
Clear infra_issue and garbage failures too, not just not_related
5e2a080
Shorten the suppressed-failures section and put it under NEW FAILURES
b884c7f
Say only what the advisor actually judged, and stop citing a cap that…
cdc5209
Make the suppression flag inert without the comment flag
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Deployment flags for the two Dr.CI advisor consumers. | ||
| // | ||
| // Separate from advisorConfig.ts because that module is pure data with no | ||
| // server-only behavior and is imported by React components; these read | ||
| // process.env, which a client caller would see as unset. Separate from the | ||
| // consumers themselves so that deciding whether to read verdicts at all costs | ||
| // no import of the modules that consume them -- advisorComment reaches the AWS | ||
| // SDK through its dispatch-state dependency. | ||
|
|
||
| import { isAdvisorEnabled } from "lib/advisor/advisorConfig"; | ||
|
|
||
| // Render the inline "AI verdict:" line in the Dr.CI comment. Its own flag so it | ||
| // ships dark and can be enabled per deployment (Vercel env var), independently | ||
| // of the auto-dispatch flag. Display-only, so it doesn't also require | ||
| // VERCEL_ENV (unlike auto-dispatch, which fires real workflow_dispatches). | ||
| export function advisorCommentEnabled(owner: string, repo: string): boolean { | ||
| return ( | ||
| process.env.DRCI_ADVISOR_COMMENT_ENABLED === "true" && | ||
| isAdvisorEnabled(owner, repo) | ||
| ); | ||
| } | ||
|
|
||
| // Move advisor-cleared failures out of the blocking set. Kept separate from the | ||
| // display flag above because this one decides whether a merge is allowed, not | ||
| // just what the comment says, so the display half can be enabled on its own | ||
| // while the merge gate stays off. | ||
| // | ||
| // REQUIRES the comment flag, and is inert without it. Suppression with the | ||
| // comment off would move a job into the non-blocking section while | ||
| // buildAdvisorVerdictLines returns nothing, so the reader is told the AI | ||
| // cleared the job but never which verdict cleared it or why. That combination | ||
| // has no use -- a merge gate the comment cannot account for -- so it is | ||
| // unreachable by construction rather than left to deployment discipline. | ||
| export function advisorSuppressionEnabled( | ||
| owner: string, | ||
| repo: string | ||
| ): boolean { | ||
| return ( | ||
| process.env.DRCI_ADVISOR_SUPPRESSION_ENABLED === "true" && | ||
|
Contributor
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. Nit: the agent recommends me against having 2 flags like this as it opens the door to mistake like setting |
||
| advisorCommentEnabled(owner, repo) | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| // Decides which NEW/unclassified failures the AI CI Advisor has cleared well | ||
| // enough to stop blocking a merge. | ||
| // | ||
| // Deliberately separate from advisorComment.ts. That module renders a display | ||
| // line and is gated by a display flag; this one moves a failure out of the | ||
| // blocking set, so it gets its own flag and a strictly narrower predicate. | ||
| // Everything here is fail-safe: any missing, stale, ambiguous or low-confidence | ||
| // verdict leaves the job blocking. | ||
|
|
||
| import dayjs from "dayjs"; | ||
| import utc from "dayjs/plugin/utc"; | ||
| import { | ||
| confidenceBucket, | ||
| drciSignalKeyForJob, | ||
| } from "lib/advisor/advisorBadge"; | ||
| import { advisorSuppressionEnabled } from "lib/advisor/advisorFlags"; | ||
| import { | ||
| AdvisorVerdictRow, | ||
| AdvisorVerdictType, | ||
| headRowsBySignalKey, | ||
| resolveVerdict, | ||
| } from "lib/advisorVerdictUtils"; | ||
| import { isTime0 } from "lib/bot/utils"; | ||
| import { RecentWorkflowsData } from "lib/types"; | ||
|
|
||
| dayjs.extend(utc); | ||
|
|
||
| // Every advisor verdict and whether it stops a failure from blocking. | ||
| // | ||
| // Written as a TOTAL map rather than a list of the cleared ones: adding a member | ||
| // to AdvisorVerdictType then fails to compile here until someone decides which | ||
| // side it falls on, which is the decision most likely to be skipped. | ||
| // | ||
| // not_related the failure exists independently of this change | ||
| // infra_issue the environment broke -- credentials, image pull, runner loss | ||
| // garbage the signal itself is noise: it flips red/green across unrelated | ||
| // commits, and the baselines fail the same way | ||
| // related the opposite claim -- this change caused it | ||
| // revert the trunk-side spelling of `related` | ||
| // unsure no claim was reached | ||
| // | ||
| // `garbage` sits with the cleared ones rather than with `unsure` because it is | ||
| // an EVIDENCED claim about the signal, not an absence of one: the advisor | ||
| // reaches it by comparing the job against its own baselines. `unsure` is the | ||
| // real "cannot tell", and it blocks. | ||
| // | ||
| // LIMIT, stated because the confidence and freshness gates do not cover it: a | ||
| // verdict says the failure looks environmental, not that the PR is innocent of | ||
| // causing it. A change to CI config, a Dockerfile, or a submodule pin can | ||
| // produce a genuine infrastructure-shaped failure that IS the PR's fault, and | ||
| // `infra_issue` is where that lands. `producedATestOutcome` below narrows this | ||
| // -- it gates on the job's own conclusion rather than the advisor's opinion, so | ||
| // an infra fault that left the job `cancelled` never reaches here -- but it does | ||
| // not close it, since a job can conclude `failure` having tested nothing. | ||
| // Nothing on this side bounds how many such jobs one merge may skip; a | ||
| // per-merge cap is proposed in pytorch/pytorch#195503. Unless such a cap is | ||
| // deployed, the flag being off is what holds this residue. | ||
| const VERDICT_DISPOSITION: Record<AdvisorVerdictType, "clear" | "block"> = { | ||
| not_related: "clear", | ||
| infra_issue: "clear", | ||
| garbage: "clear", | ||
| related: "block", | ||
| revert: "block", | ||
| unsure: "block", | ||
| }; | ||
|
|
||
| export const SUPPRESSIBLE_VERDICTS: ReadonlySet<string> = new Set( | ||
| Object.entries(VERDICT_DISPOSITION) | ||
| .filter(([, disposition]) => disposition === "clear") | ||
| .map(([verdict]) => verdict) | ||
| ); | ||
|
|
||
| // Clear only what the badge scale calls high confidence. Asks advisorBadge for | ||
| // the bucket rather than restating its threshold, so retuning the scale moves | ||
| // the gate with it. | ||
| // | ||
| // That keeps badge and gate in step for `not_related` -- the only cleared | ||
| // verdict whose label is hedged by confidence, so it is the only one that could | ||
| // read "probably not related" while this suppressed. `verdictBadge` returns for | ||
| // `garbage` and `infra_issue` before it consults the bucket, so those labels | ||
| // never hedge: a sub-threshold `infra_issue` shows a flat "infra issue" beside a | ||
| // job that still blocks, and about 6% of `infra_issue` rows in a recent 30-day | ||
| // sample sit below the bar. Fixing that means changing labels in advisorBadge.ts, | ||
| // which is on the already-live comment path -- a follow-up, not this change. | ||
| export function confidentEnoughToSuppress(confidence: number): boolean { | ||
| return confidenceBucket(confidence) === "high"; | ||
| } | ||
|
|
||
| // A verdict describes one execution of a job, but is keyed only by (sha, job | ||
| // name), so a rerun at the same head would otherwise inherit the previous run's | ||
| // verdict. Requiring the verdict to be strictly newer than the job's completion | ||
| // rejects the common case: the rerun finishes after the old verdict was | ||
| // written, so the job blocks until a fresh analysis lands. Equality is treated | ||
| // as stale because these timestamps are truncated and a tie cannot be told | ||
| // apart from the unsafe ordering. | ||
| // | ||
| // KNOWN GAP, and the reason this ships behind a flag: a verdict for execution A | ||
| // that lands after execution B has finished still passes this test. Closing it | ||
| // needs the analyzed job id recorded on the verdict row -- the row's `run_id` | ||
| // is the advisor's own dispatch run, not the job's. | ||
| function verdictDescribesThisRun( | ||
| job: RecentWorkflowsData, | ||
| verdictTimestamp: string | ||
| ): boolean { | ||
| // isTime0 covers the epoch sentinel AND unparseable input (it NaN-checks), so | ||
| // a malformed timestamp on either side blocks rather than passing. | ||
| if (isTime0(job.completed_at) || isTime0(verdictTimestamp)) { | ||
| return false; | ||
| } | ||
| return dayjs.utc(verdictTimestamp).isAfter(dayjs.utc(job.completed_at)); | ||
| } | ||
|
|
||
| // Only a conclusive `failure` is eligible. `cancelled`, `timed_out`, | ||
| // `action_required` and friends did not produce a test outcome, and the advisor | ||
| // labels much of that class `not_related` anyway -- see the shadow-mode | ||
| // adjudication. This narrows that exposure; it does not close it, because a | ||
| // job can also fail without a real outcome (runner lost, driver fault) while | ||
| // still concluding `failure`. That residue is what the manual adjudication | ||
| // before enforcement is for. | ||
| export function producedATestOutcome(job: RecentWorkflowsData): boolean { | ||
| return job.conclusion === "failure"; | ||
| } | ||
|
|
||
| /** Whether one job's verdict clears it to stop blocking. Pure, for testing. */ | ||
| export function isSuppressible( | ||
| job: RecentWorkflowsData, | ||
| rows: AdvisorVerdictRow[] | ||
| ): boolean { | ||
| if (!producedATestOutcome(job)) { | ||
| return false; | ||
| } | ||
| const resolved = resolveVerdict(rows); | ||
| if (resolved === null) { | ||
| return false; | ||
| } | ||
| return ( | ||
| SUPPRESSIBLE_VERDICTS.has(resolved.verdict) && | ||
| confidentEnoughToSuppress(resolved.confidence) && | ||
| verdictDescribesThisRun(job, resolved.timestamp) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Job ids among `jobs` that the advisor has cleared. Returns an empty set when | ||
| * the flag is off, so the caller needs no separate check. | ||
| * | ||
| * Takes the PR's verdict rows rather than reading them: drci.ts reads once and | ||
| * shares them with the badge line, so the two cannot resolve the same job to | ||
| * different verdicts. They can still reach different DISPOSITIONS -- the gate | ||
| * adds the confidence, freshness and conclusion tests the badge does not, so a | ||
| * confident-looking badge beside a still-blocking job is expected. Rows for any | ||
| * other commit are dropped here, so a verdict from an earlier head can never | ||
| * clear a job at this one. | ||
| */ | ||
| export function suppressibleJobIds( | ||
| owner: string, | ||
| repo: string, | ||
| headSha: string, | ||
| jobs: RecentWorkflowsData[], | ||
| verdictRows: AdvisorVerdictRow[] | ||
| ): Set<number> { | ||
| if (!advisorSuppressionEnabled(owner, repo) || jobs.length === 0) { | ||
| return new Set(); | ||
| } | ||
|
|
||
| const rowsByKey = headRowsBySignalKey(verdictRows, headSha); | ||
|
|
||
| const suppressible = new Set<number>(); | ||
| for (const job of jobs) { | ||
| if (!job.name) { | ||
| continue; | ||
| } | ||
| const rows = rowsByKey.get(drciSignalKeyForJob(job.name)) ?? []; | ||
| if (isSuppressible(job, rows)) { | ||
| suppressible.add(job.id); | ||
| } | ||
| } | ||
| return suppressible; | ||
| } | ||
|
|
||
| /** | ||
| * Move cleared jobs out of `blocking` and return them, mutating the array in | ||
| * place. In place because drci.ts hands the same array object to both the | ||
| * failures dict and the comment renderer, and CRCR L4 pushes into it later -- | ||
| * rebinding would silently desync those. | ||
| */ | ||
| export function extractSuppressed( | ||
| blocking: RecentWorkflowsData[], | ||
| suppressibleIds: Set<number> | ||
| ): RecentWorkflowsData[] { | ||
| const extracted: RecentWorkflowsData[] = []; | ||
| for (let i = blocking.length - 1; i >= 0; i--) { | ||
| if (suppressibleIds.has(blocking[i].id)) { | ||
| extracted.unshift(blocking[i]); | ||
| blocking.splice(i, 1); | ||
| } | ||
| } | ||
| return extracted; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // The single read of a PR's advisor verdicts. | ||
| // | ||
| // Both consumers in the Dr.CI request -- the inline badge line and the | ||
| // suppression gate -- need the same rows for the same PR and head. Reading | ||
| // twice cost a ClickHouse round trip per PR and, worse, let a verdict landing | ||
| // between the two reads make the comment and the merge gate describe the same | ||
| // job differently. drci.ts reads once here and hands the rows to both. | ||
|
|
||
| import { | ||
| advisorCommentEnabled, | ||
| advisorSuppressionEnabled, | ||
| } from "lib/advisor/advisorFlags"; | ||
| import { AdvisorVerdictRow } from "lib/advisorVerdictUtils"; | ||
| import { queryClickhouseSaved } from "lib/clickhouse"; | ||
| import { RecentWorkflowsData } from "lib/types"; | ||
|
|
||
| /** | ||
| * Whether this PR is worth a verdict read at all. | ||
| * | ||
| * Both consumers bail on an empty job list, so a PR with no new or | ||
| * unclassified failures -- the common case -- must not cost a query. Each | ||
| * consumer re-checks its own flag; this only decides whether to read. | ||
| */ | ||
| export function shouldReadAdvisorVerdicts( | ||
| owner: string, | ||
| repo: string, | ||
| jobs: RecentWorkflowsData[] | ||
| ): boolean { | ||
| if (jobs.length === 0) { | ||
| return false; | ||
| } | ||
| return ( | ||
| advisorCommentEnabled(owner, repo) || advisorSuppressionEnabled(owner, repo) | ||
| ); | ||
| } | ||
|
|
||
| export async function fetchAdvisorVerdictRows( | ||
| owner: string, | ||
| repo: string, | ||
| prNumber: number | ||
| ): Promise<AdvisorVerdictRow[]> { | ||
| return (await queryClickhouseSaved("advisor_verdicts_for_pr", { | ||
| repo: `${owner}/${repo}`, | ||
| prNumber, | ||
| })) as AdvisorVerdictRow[]; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
When this
DRCI_ADVISOR_COMMENT_ENABLEDflag is off, let's make sure that trymerge can work like it's today.