| #!/usr/bin/env node |
|
|
| |
| |
| |
| |
| |
|
|
| import fs from 'node:fs'; |
| import path from 'node:path'; |
| import { execSync } from 'node:child_process'; |
| import os from 'node:os'; |
|
|
| const artifactsDir = process.argv[2] || '.'; |
| const MAX_HISTORY = 10; |
|
|
| |
| function findReports(dir) { |
| const reports = []; |
| if (!fs.existsSync(dir)) return reports; |
|
|
| const files = fs.readdirSync(dir); |
| for (const file of files) { |
| const fullPath = path.join(dir, file); |
| const stat = fs.statSync(fullPath); |
| if (stat.isDirectory()) { |
| reports.push(...findReports(fullPath)); |
| } else if (file === 'report.json') { |
| reports.push(fullPath); |
| } |
| } |
| return reports; |
| } |
|
|
| function getModelFromPath(reportPath) { |
| const parts = reportPath.split(path.sep); |
| |
| const artifactDir = parts.find((p) => p.startsWith('eval-logs-')); |
| if (!artifactDir) return 'unknown'; |
|
|
| const matchNew = artifactDir.match(/^eval-logs-(.+)-(\d+)$/); |
| if (matchNew) return matchNew[1]; |
|
|
| const matchOld = artifactDir.match(/^eval-logs-(\d+)$/); |
| if (matchOld) return 'gemini-2.5-pro'; |
|
|
| return 'unknown'; |
| } |
|
|
| function getStats(reports) { |
| |
| const statsByModel = {}; |
|
|
| for (const reportPath of reports) { |
| try { |
| const model = getModelFromPath(reportPath); |
| if (!statsByModel[model]) { |
| statsByModel[model] = {}; |
| } |
| const testStats = statsByModel[model]; |
|
|
| const content = fs.readFileSync(reportPath, 'utf-8'); |
| const json = JSON.parse(content); |
|
|
| for (const testResult of json.testResults) { |
| for (const assertion of testResult.assertionResults) { |
| const name = assertion.title; |
| if (!testStats[name]) { |
| testStats[name] = { passed: 0, failed: 0, total: 0 }; |
| } |
| testStats[name].total++; |
| if (assertion.status === 'passed') { |
| testStats[name].passed++; |
| } else { |
| testStats[name].failed++; |
| } |
| } |
| } |
| } catch (error) { |
| console.error(`Error processing report at ${reportPath}:`, error); |
| } |
| } |
| return statsByModel; |
| } |
|
|
| function fetchHistoricalData() { |
| const history = []; |
|
|
| try { |
| |
| const branch = 'main'; |
|
|
| |
| const cmd = `gh run list --workflow evals-nightly.yml --branch "${branch}" --limit ${ |
| MAX_HISTORY + 5 |
| } --json databaseId,createdAt,url,displayTitle,status,conclusion`; |
| const runsJson = execSync(cmd, { encoding: 'utf-8' }); |
| let runs = JSON.parse(runsJson); |
|
|
| |
| const currentRunId = process.env.GITHUB_RUN_ID; |
| if (currentRunId) { |
| runs = runs.filter((r) => r.databaseId.toString() !== currentRunId); |
| } |
|
|
| |
| |
| runs = runs.filter((r) => r.status === 'completed').slice(0, MAX_HISTORY); |
|
|
| |
| for (const run of runs) { |
| const tmpDir = fs.mkdtempSync( |
| path.join(os.tmpdir(), `gemini-evals-${run.databaseId}-`), |
| ); |
| try { |
| |
| |
| |
| execSync( |
| `gh run download ${run.databaseId} -p "eval-logs-*" -D "${tmpDir}"`, |
| { stdio: 'ignore' }, |
| ); |
|
|
| const runReports = findReports(tmpDir); |
| if (runReports.length > 0) { |
| history.push({ |
| run, |
| stats: getStats(runReports), |
| }); |
| } |
| } catch (error) { |
| console.error( |
| `Failed to download or process artifacts for run ${run.databaseId}:`, |
| error, |
| ); |
| } finally { |
| fs.rmSync(tmpDir, { recursive: true, force: true }); |
| } |
| } |
| } catch (error) { |
| console.error('Failed to fetch historical data:', error); |
| } |
|
|
| return history; |
| } |
|
|
| function generateMarkdown(currentStatsByModel, history) { |
| console.log('### Evals Nightly Summary\n'); |
| console.log( |
| 'See [evals/README.md](https://github.com/google-gemini/gemini-cli/tree/main/evals) for more details.\n', |
| ); |
|
|
| |
| const reversedHistory = [...history].reverse(); |
|
|
| const models = Object.keys(currentStatsByModel).sort(); |
|
|
| const getPassRate = (statsForModel) => { |
| if (!statsForModel) return '-'; |
| const totalStats = Object.values(statsForModel).reduce( |
| (acc, stats) => { |
| acc.passed += stats.passed; |
| acc.total += stats.total; |
| return acc; |
| }, |
| { passed: 0, total: 0 }, |
| ); |
| return totalStats.total > 0 |
| ? ((totalStats.passed / totalStats.total) * 100).toFixed(1) + '%' |
| : '-'; |
| }; |
|
|
| for (const model of models) { |
| const currentStats = currentStatsByModel[model]; |
| const totalPassRate = getPassRate(currentStats); |
|
|
| console.log(`#### Model: ${model}`); |
| console.log(`**Total Pass Rate: ${totalPassRate}**\n`); |
|
|
| |
| let header = '| Test Name |'; |
| let separator = '| :--- |'; |
| let passRateRow = '| **Overall Pass Rate** |'; |
|
|
| for (const item of reversedHistory) { |
| header += ` [${item.run.databaseId}](${item.run.url}) |`; |
| separator += ' :---: |'; |
| passRateRow += ` **${getPassRate(item.stats[model])}** |`; |
| } |
|
|
| |
| header += ' Current |'; |
| separator += ' :---: |'; |
| passRateRow += ` **${totalPassRate}** |`; |
|
|
| console.log(header); |
| console.log(separator); |
| console.log(passRateRow); |
|
|
| |
| const allTestNames = new Set(Object.keys(currentStats)); |
| for (const item of reversedHistory) { |
| if (item.stats[model]) { |
| Object.keys(item.stats[model]).forEach((name) => |
| allTestNames.add(name), |
| ); |
| } |
| } |
|
|
| for (const name of Array.from(allTestNames).sort()) { |
| const searchUrl = `https://github.com/search?q=repo%3Agoogle-gemini%2Fgemini-cli%20%22${encodeURIComponent(name)}%22&type=code`; |
| let row = `| [${name}](${searchUrl}) |`; |
|
|
| |
| for (const item of reversedHistory) { |
| const stat = item.stats[model] ? item.stats[model][name] : null; |
| if (stat) { |
| const passRate = ((stat.passed / stat.total) * 100).toFixed(0) + '%'; |
| row += ` ${passRate} |`; |
| } else { |
| row += ' - |'; |
| } |
| } |
|
|
| |
| const curr = currentStats[name]; |
| if (curr) { |
| const passRate = ((curr.passed / curr.total) * 100).toFixed(0) + '%'; |
| row += ` ${passRate} |`; |
| } else { |
| row += ' - |'; |
| } |
|
|
| console.log(row); |
| } |
| console.log('\n'); |
| } |
| } |
|
|
| |
|
|
| const currentReports = findReports(artifactsDir); |
| if (currentReports.length === 0) { |
| console.log('No reports found.'); |
| |
| |
| |
| process.exit(0); |
| } |
|
|
| const currentStats = getStats(currentReports); |
| const history = fetchHistoricalData(); |
| generateMarkdown(currentStats, history); |
|
|