| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import {execSync} from 'node:child_process'; | |
| import {parseArgs} from 'node:util'; | |
| export function getLuciToken(): string { | |
| try { | |
| return execSync('luci-auth token', {encoding: 'utf-8'}).trim(); | |
| } catch { | |
| console.error('Error: Could not get LUCI auth token. Did you run \'luci-auth login\'?'); | |
| process.exit(1); | |
| } | |
| } | |
| interface ClusterMetric { | |
| value: string; | |
| } | |
| interface ClusterMetrics { | |
| 'critical-failures-exonerated'?: ClusterMetric; | |
| failures?: ClusterMetric; | |
| 'human-cls-failed-presubmit'?: ClusterMetric; | |
| } | |
| interface ClusterId { | |
| id: string; | |
| algorithm: string; | |
| } | |
| export interface Cluster { | |
| title?: string; | |
| clusterId?: ClusterId; | |
| metrics?: ClusterMetrics; | |
| bug?: {url?: string}; | |
| } | |
| export async function getFlakyClusters(days = 1): Promise<Cluster[]> { | |
| const url = 'https://analysis.api.luci.app/prpc/luci.analysis.v1.Clusters/QueryClusterSummaries'; | |
| const projectName = 'devtools-frontend'; | |
| const headers = { | |
| Authorization: `Bearer ${getLuciToken()}`, | |
| Accept: 'application/json', | |
| 'Content-Type': 'application/json', | |
| }; | |
| const now = new Date(); | |
| const earliest = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000)); | |
| const payload = { | |
| project: projectName, | |
| orderBy: 'metrics.`test-run-failed-in-postsubmit`.value desc', | |
| metrics: [ | |
| `projects/${projectName}/metrics/critical-failures-exonerated`, | |
| `projects/${projectName}/metrics/failures`, | |
| `projects/${projectName}/metrics/test-run-failed-in-postsubmit`, | |
| `projects/${projectName}/metrics/human-cls-failed-presubmit`, | |
| ], | |
| timeRange: { | |
| earliest: earliest.toISOString(), | |
| latest: now.toISOString(), | |
| }, | |
| }; | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| const text = await response.text(); | |
| console.error(`HTTP Error ${response.status}: ${response.statusText}`); | |
| console.error(text); | |
| throw new Error(`Network error querying LUCI Analysis: ${response.statusText}`); | |
| } | |
| let rawData = await response.text(); | |
| if (rawData.startsWith(')]}\'\n')) { | |
| rawData = rawData.substring(5); | |
| } | |
| const data = JSON.parse(rawData); | |
| let clusters: Cluster[] = data.clusterSummaries || []; | |
| const isChromiumTest = (title: string) => | |
| title.includes('ninja://') || title.includes('Unexpected Diff') || title.includes('.cc('); | |
| clusters = clusters.filter(c => !isChromiumTest(c.title || '')); | |
| clusters = clusters.filter(c => Number(c.metrics?.failures?.value || 0) > 0); | |
| return clusters; | |
| } | |
| async function main(): Promise<void> { | |
| const {values} = parseArgs({ | |
| options: { | |
| format: { | |
| type: 'string', | |
| }, | |
| }, | |
| }); | |
| const isJson = values.format === 'json'; | |
| if (!isJson) { | |
| console.log('Fetching flaky clusters for devtools-frontend over the last 24 hours...'); | |
| } | |
| const clusters = await getFlakyClusters(1); | |
| if (isJson) { | |
| console.log(JSON.stringify(clusters, null, 2)); | |
| } else { | |
| console.log('\nFlaky Clusters for devtools-frontend:'); | |
| console.log('='.repeat(60)); | |
| for (const cluster of clusters) { | |
| const title = cluster.title || 'No Title'; | |
| const clusterId = cluster.clusterId?.id || 'Unknown ID'; | |
| const algorithm = cluster.clusterId?.algorithm || 'Unknown Algorithm'; | |
| const totalFailures = cluster.metrics?.['test-run-failed-in-postsubmit']?.value || 0; | |
| const clusterLink = `https://luci-analysis.appspot.com/p/devtools-frontend/clusters/${algorithm}/${clusterId}`; | |
| console.log(`Cluster: ${title}`); | |
| console.log(`Link: ${clusterLink}`); | |
| console.log(`Impact: ${totalFailures} total failures`); | |
| console.log('-'.repeat(60)); | |
| } | |
| } | |
| } | |
| if (import.meta.main) { | |
| main().catch(err => { | |
| console.error(err); | |
| process.exit(1); | |
| }); | |
| } |