diff --git a/torchci/clickhouse_queries/autorevert_commits/params.json b/torchci/clickhouse_queries/autorevert_commits/params.json new file mode 100644 index 0000000000..4b82f61639 --- /dev/null +++ b/torchci/clickhouse_queries/autorevert_commits/params.json @@ -0,0 +1,12 @@ +{ + "params": { + "repo": "String", + "shas": "Array(String)" + }, + "tests": [ + { + "repo": "pytorch/pytorch", + "shas": ["abc123", "def456"] + } + ] +} diff --git a/torchci/clickhouse_queries/autorevert_commits/query.sql b/torchci/clickhouse_queries/autorevert_commits/query.sql new file mode 100644 index 0000000000..1b975faede --- /dev/null +++ b/torchci/clickhouse_queries/autorevert_commits/query.sql @@ -0,0 +1,11 @@ +SELECT + commit_sha, + groupArray(workflows) as all_workflows, + groupArray(source_signal_keys) as all_source_signal_keys +FROM misc.autorevert_events_v2 +WHERE repo = {repo: String} + AND action = 'revert' + AND dry_run = 0 + AND failed = 0 + AND commit_sha IN {shas: Array(String)} +GROUP BY commit_sha diff --git a/torchci/clickhouse_queries/autorevert_details/params.json b/torchci/clickhouse_queries/autorevert_details/params.json new file mode 100644 index 0000000000..a6b7ced1af --- /dev/null +++ b/torchci/clickhouse_queries/autorevert_details/params.json @@ -0,0 +1,12 @@ +{ + "params": { + "repo": "String", + "sha": "String" + }, + "tests": [ + { + "repo": "pytorch/pytorch", + "sha": "321e60abc123" + } + ] +} \ No newline at end of file diff --git a/torchci/clickhouse_queries/autorevert_details/query.sql b/torchci/clickhouse_queries/autorevert_details/query.sql new file mode 100644 index 0000000000..91c9e748d5 --- /dev/null +++ b/torchci/clickhouse_queries/autorevert_details/query.sql @@ -0,0 +1,13 @@ +SELECT + commit_sha, + workflows, + source_signal_keys, + ts +FROM misc.autorevert_events_v2 +WHERE + repo = {repo: String} + AND commit_sha = {sha: String} + AND action = 'revert' + AND dry_run = 0 + AND failed = 0 +ORDER BY ts DESC diff --git a/torchci/components/commit/AutorevertBanner.module.css b/torchci/components/commit/AutorevertBanner.module.css new file mode 100644 index 0000000000..9802357ee9 --- /dev/null +++ b/torchci/components/commit/AutorevertBanner.module.css @@ -0,0 +1,52 @@ +.autorevertBanner { + background-color: var(--sev-banner-bg); + border: 2px solid var(--autoreverted-signal-border); + border-radius: 8px; + padding: 16px; + margin: 16px 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.bannerHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 1.1em; +} + +.warningIcon { + font-size: 1.2em; +} + +.bannerContent { + color: var(--text-color); +} + +.bannerContent p { + margin: 8px 0; +} + +.workflowList { + margin: 12px 0; + padding-left: 24px; +} + +.workflowList li { + margin: 6px 0; + list-style-type: disc; +} + +.workflowList a { + color: var(--link-color); + text-decoration: none; +} + +.workflowList a:hover { + text-decoration: underline; +} + +.investigateMessage { + font-style: italic; + margin-top: 12px; +} diff --git a/torchci/components/commit/AutorevertBanner.tsx b/torchci/components/commit/AutorevertBanner.tsx new file mode 100644 index 0000000000..d0dccf2941 --- /dev/null +++ b/torchci/components/commit/AutorevertBanner.tsx @@ -0,0 +1,133 @@ +import useSWR from "swr"; +import styles from "./AutorevertBanner.module.css"; + +interface AutorevertDetails { + commit_sha: string; + workflows: string[]; + source_signal_keys: string[]; + job_ids: number[]; + job_base_names: string[]; + wf_run_ids: number[]; + created_at: string; +} + +interface SignalInfo { + workflow_name: string; + signals: Array<{ + key: string; + job_url?: string; + hud_url?: string; + }>; +} + +export function AutorevertBanner({ + repoOwner, + repoName, + sha, +}: { + repoOwner: string; + repoName: string; + sha: string; +}) { + const { data: autorevertData, error } = useSWR( + `/api/autorevert/${repoOwner}/${repoName}/${sha}`, + async (url) => { + try { + const response = await fetch(url); + + if (response.status === 404) { + // No autorevert data for this commit + return null; + } + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + return data; + } catch (e) { + // Silently fail - no autorevert data + return null; + } + }, + { + refreshInterval: 0, // Don't refresh autorevert data + } + ); + + // Don't show banner if no data or error + if (!autorevertData) { + return null; + } + + // Handle case where arrays might be undefined or have different structure + const workflows = autorevertData.workflows || []; + const sourceSignalKeys = autorevertData.source_signal_keys || []; + const jobIds = autorevertData.job_ids || []; + const wfRunIds = autorevertData.wf_run_ids || []; + const jobBaseNames = autorevertData.job_base_names || []; + + // If no workflows data, don't show the banner + if (!workflows.length) { + return null; + } + + // Group signals by workflow + const signalsByWorkflow = new Map(); + + workflows.forEach((workflow, idx) => { + if (!signalsByWorkflow.has(workflow)) { + signalsByWorkflow.set(workflow, { + workflow_name: workflow, + signals: [], + }); + } + + const signalKey = sourceSignalKeys[idx] || ""; + + const signal = { + key: signalKey, + // Since we don't have job IDs in the table, we can't create direct job links + job_url: undefined, + // Try to create a HUD URL using the signal key as a filter + hud_url: signalKey + ? `/hud/${repoOwner}/${repoName}/main?nameFilter=${encodeURIComponent( + signalKey + )}` + : undefined, + }; + + signalsByWorkflow.get(workflow)!.signals.push(signal); + }); + + return ( +
+
+ ⚠️ + This commit was automatically reverted +
+
+

This PR is attributed to have caused regression in:

+
    + {Array.from(signalsByWorkflow.values()).map((workflowInfo) => ( +
  • + {workflowInfo.workflow_name}:{" "} + {workflowInfo.signals.map((signal, idx) => ( + + {idx > 0 && ", "} + {signal.key} + + ))} +
  • + ))} +
+

+ You can add the label autorevert: disable to disable + autorevert for a specific PR. +

+
+
+ ); +} diff --git a/torchci/components/hud.module.css b/torchci/components/hud.module.css index d189e3918d..568196dcd5 100644 --- a/torchci/components/hud.module.css +++ b/torchci/components/hud.module.css @@ -132,6 +132,15 @@ background-color: var(--forced-merge-failure-bg); } +.autoreverted { + background-color: var(--autoreverted-bg); +} + +.autorevertSignal { + background-color: var(--autoreverted-signal-bg); + border: 2px solid var(--autoreverted-signal-border); +} + .selectedRow { background-color: var(--selected-row-bg); font-weight: bold; diff --git a/torchci/lib/fetchHud.ts b/torchci/lib/fetchHud.ts index 3250e74d10..96d9a90e65 100644 --- a/torchci/lib/fetchHud.ts +++ b/torchci/lib/fetchHud.ts @@ -77,6 +77,28 @@ export default async function fetchHud( ) ); + // Check if any of these commits were autoreverted + const autorevertedCommits = await queryClickhouseSaved("autorevert_commits", { + repo: `${params.repoOwner}/${params.repoName}`, + shas: shas, + }); + + // Create a map from sha to autorevert data + const autorevertDataBySha = new Map< + string, + { workflows: string[]; signals: string[] } + >(); + autorevertedCommits.forEach((r) => { + // Flatten the nested arrays + const allWorkflows = r.all_workflows.flat(); + const allSignals = r.all_source_signal_keys.flat(); + + autorevertDataBySha.set(r.commit_sha, { + workflows: allWorkflows, + signals: allSignals, + }); + }); + const commitsBySha = _.keyBy(commits, "sha"); if (params.filter_reruns) { @@ -151,11 +173,15 @@ export default async function fetchHud( } } + const autorevertData = autorevertDataBySha.get(commit.sha); const row = { ...commit, jobs: jobs, isForcedMerge: forcedMergeShas.has(commit.sha), isForcedMergeWithFailures: forcedMergeWithFailuresShas.has(commit.sha), + isAutoreverted: autorevertData !== undefined, + autorevertWorkflows: autorevertData?.workflows, + autorevertSignals: autorevertData?.signals, }; shaGrid.push(row); }); diff --git a/torchci/lib/types.ts b/torchci/lib/types.ts index 6d0d3e80c9..ced8e65339 100644 --- a/torchci/lib/types.ts +++ b/torchci/lib/types.ts @@ -90,6 +90,9 @@ export interface Highlight { interface RowDataBase extends CommitData { isForcedMerge: boolean | false; isForcedMergeWithFailures: boolean | false; + isAutoreverted: boolean | false; + autorevertWorkflows?: string[]; + autorevertSignals?: string[]; } export interface RowData extends RowDataBase { diff --git a/torchci/lib/utilization/fetchUtilization.test.ts b/torchci/lib/utilization/fetchUtilization.test.ts index ad39a6a82a..cf744e7e7f 100644 --- a/torchci/lib/utilization/fetchUtilization.test.ts +++ b/torchci/lib/utilization/fetchUtilization.test.ts @@ -213,7 +213,7 @@ describe("Test flattenTS to flatten timestamp", () => { // assert log expect(logSpy).toHaveBeenCalledWith( - `Warning: Error parsing JSON:SyntaxError: Expected property name or '}' in JSON at position 1 for data string '{{}dsad}'` + `Warning: Error parsing JSON:SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2) for data string '{{}dsad}'` ); }); }); diff --git a/torchci/pages/[repoOwner]/[repoName]/commit/[sha].tsx b/torchci/pages/[repoOwner]/[repoName]/commit/[sha].tsx index 94b6b5b3a9..d3b4cebe6b 100644 --- a/torchci/pages/[repoOwner]/[repoName]/commit/[sha].tsx +++ b/torchci/pages/[repoOwner]/[repoName]/commit/[sha].tsx @@ -1,3 +1,4 @@ +import { AutorevertBanner } from "components/commit/AutorevertBanner"; import { CommitInfo } from "components/commit/CommitInfo"; import { useSetTitle } from "components/layout/DynamicTitle"; import { useRouter } from "next/router"; @@ -24,12 +25,19 @@ export default function Page() { {fancyName} Commit: {sha} {sha !== undefined && ( - + <> + + + )} ); diff --git a/torchci/pages/api/autorevert/[repoOwner]/[repoName]/[sha].ts b/torchci/pages/api/autorevert/[repoOwner]/[repoName]/[sha].ts new file mode 100644 index 0000000000..483724f676 --- /dev/null +++ b/torchci/pages/api/autorevert/[repoOwner]/[repoName]/[sha].ts @@ -0,0 +1,53 @@ +import { queryClickhouseSaved } from "lib/clickhouse"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { repoOwner, repoName, sha } = req.query; + + if (!repoOwner || !repoName || !sha) { + res.status(400).json({ error: "Missing required parameters" }); + return; + } + + try { + const results = await queryClickhouseSaved("autorevert_details", { + repo: `${repoOwner}/${repoName}`, + sha: sha as string, + }); + + if (results && results.length > 0) { + // Combine data from all rows (in case there are multiple events) + const allWorkflows: string[] = []; + const allSignalKeys: string[] = []; + + results.forEach((row: any) => { + if (row.workflows) allWorkflows.push(...row.workflows); + if (row.source_signal_keys) + allSignalKeys.push(...row.source_signal_keys); + }); + + const response = { + commit_sha: results[0].commit_sha, + workflows: allWorkflows, + source_signal_keys: allSignalKeys, + // These fields don't exist in the table, so we'll use empty arrays + job_ids: [], + job_base_names: [], + wf_run_ids: [], + }; + + res.status(200).json(response); + } else { + res.status(404).json({ error: "No autorevert data found" }); + } + } catch (error: any) { + res.status(500).json({ + error: "Internal server error", + details: + process.env.NODE_ENV === "development" ? error.message : undefined, + }); + } +} diff --git a/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx b/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx index ff0c8f60c4..2eeeafef27 100644 --- a/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx +++ b/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx @@ -62,15 +62,28 @@ export function JobCell({ sha, job, unstableIssues, + isAutorevertSignal, }: { sha: string; job: JobData; unstableIssues: IssueData[]; + isAutorevertSignal?: boolean; }) { const [pinnedId, setPinnedId] = useContext(PinnedTooltipContext); - const style = pinnedId.name == job.name ? styles.highlight : ""; + let cellStyle = ""; + if (pinnedId.name == job.name) { + cellStyle = styles.highlight; + } else if (isAutorevertSignal) { + cellStyle = styles.autorevertSignal; + } + return ( - window.open(job.htmlUrl)}> + window.open(job.htmlUrl)} + title={ + isAutorevertSignal ? "This job triggered an autorevert" : undefined + } + > -
+
clickCommit(e)}> + clickCommit(e)} + title={ + rowData.isAutoreverted ? "This commit was autoreverted" : undefined + } + > @@ -246,12 +271,57 @@ function HudJobCells({ ); } else { const job = rowData.nameToJobs.get(name); + const jobFullName = job?.name || name; + + // Check if this job triggered the autorevert + let isAutorevertSignal = false; + + if (rowData.autorevertWorkflows && rowData.autorevertSignals) { + // Extract workflow name and job name from full name (format is "Workflow / Job Name") + const parts = jobFullName.split(" / "); + const jobWorkflow = parts[0]; + const jobNameOnly = parts.slice(1).join(" / "); // Handle cases with multiple '/' + + // Check if this job's workflow is in the list of workflows that triggered autorevert + if (rowData.autorevertWorkflows.includes(jobWorkflow)) { + // Check if this specific job is mentioned in the signals + isAutorevertSignal = rowData.autorevertSignals.some((signal) => { + // Signal key is either a test name or a job base name + // For jobs like "Lint / lintrunner-noclang / linux-job", the base name + // might be "lintrunner-noclang / linux-job" or just "lintrunner-noclang" + + // Normalize for comparison + const signalLower = signal.toLowerCase().trim(); + const jobNameLower = jobNameOnly.toLowerCase().trim(); + + // Check exact match first + if (signalLower === jobNameLower) { + return true; + } + + // Check if the signal matches the job name without shard suffix + // (e.g., "lintrunner-noclang" matches "lintrunner-noclang / linux-job") + const jobBaseParts = jobNameLower.split(" / "); + if (jobBaseParts.length > 1) { + const jobBaseOnly = jobBaseParts[0]; + if (signalLower === jobBaseOnly) { + return true; + } + } + + // Also try matching if signal is the complete job name + return jobNameLower === signalLower; + }); + } + } + return ( ); } diff --git a/torchci/styles/globals.css b/torchci/styles/globals.css index 718cd296bb..4a72f6ed0a 100644 --- a/torchci/styles/globals.css +++ b/torchci/styles/globals.css @@ -20,6 +20,9 @@ /* HUD specific colors */ --forced-merge-bg: lightyellow; --forced-merge-failure-bg: #ffe0b3; + --autoreverted-bg: #e8e8e8; + --autoreverted-signal-bg: #ffcccc; + --autoreverted-signal-border: #ff6666; --selected-row-bg: lightblue; --highlight-bg: #ffa; --commit-message-bg: whitesmoke; @@ -84,6 +87,9 @@ /* HUD specific colors - dark mode variants */ --forced-merge-bg: #7e7000; --forced-merge-failure-bg: #a06200; + --autoreverted-bg: #3a3a3a; + --autoreverted-signal-bg: #662222; + --autoreverted-signal-border: #aa4444; --selected-row-bg: #164863; --highlight-bg: #555500; --commit-message-bg: #2a2a2a;