mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 13:53:02 -07:00
1a8c7ec226
This PR implements several measures to reduce the cost of GitHub Actions usage, focusing on the highest-impact areas identified through real per-workflow minutes consumption analysis.
### Summary of Changes
1. **CI Matrix Optimization**: Reduced the `test_mac` matrix in `Testing: CI` to only run on Node.js 20.x.
- **Reason**: macOS runners (especially `macos-latest-large`) are significantly more expensive than Linux runners. Node.js 22.x and 24.x are still covered by the `test_linux` matrix, ensuring core compatibility. OS-specific issues are likely to be caught on the recommended Node.js version (20.x).
- **Impact**: Expected to reduce Mac runner usage by approximately 66% in the CI pipeline.
2. **Pulse Workflow Optimization**:
- Added a check to skip `npm ci` and subsequent steps if no reflex scripts are present in `tools/gemini-cli-bot/reflexes/scripts`.
- Reduced `fetch-depth` from 0 (full clone) to 1 (shallow clone).
- **Reason**: The Pulse workflow runs every 30 minutes. Installing dependencies when there is nothing to run is a waste of resources.
- **Impact**: Eliminates unnecessary dependency installation and reduces clone time for the Pulse workflow.
3. **Brain Workflow Optimization**:
- Reduced `fetch-depth` from 0 to 1.
- **Reason**: The Brain workflow does not require full repository history for its reasoning or metrics collection phases.
- **Impact**: Reduces clone time for the daily Brain workflow runs.
4. **Metrics Reporting Fix**:
- Implemented pagination in `tools/gemini-cli-bot/metrics/scripts/actions_spend.ts` to ensure a full 7-day window is captured.
- **Reason**: The previous 1000-run limit was causing significant under-reporting for constant-rate workflows like Pulse during CI surges, as the sample was saturated by bursty CI activity.
- **Impact**: Provides more accurate cost metrics, revealing the true scale of constant "heartbeat" costs.
### Data-Driven Justification
Analysis of the last 7 days of metrics (`actions_spend_minutes`) showed:
- **Testing: CI**: 4074 minutes (approx. 64% of total spend).
- **macOS Runners**: The primary driver of CI cost due to high per-minute rates on large runners.
- **Pulse Workflow**: Previously under-reported due to sampling limits, but now identified as a consistent baseline cost that can be significantly optimized.
These changes prioritize high-impact reductions in expensive runner minutes while maintaining robust cross-platform testing on the primary supported Node.js version.
149 lines
3.9 KiB
TypeScript
149 lines
3.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
async function getWorkflowMinutes(): Promise<Record<string, number>> {
|
|
const sevenDaysAgoDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
.toISOString()
|
|
.split('T')[0];
|
|
|
|
const workflowMinutes: Record<string, number> = {};
|
|
let token: string;
|
|
let repoName: string;
|
|
|
|
try {
|
|
token = execFileSync('gh', ['auth', 'token'], {
|
|
encoding: 'utf-8',
|
|
}).trim();
|
|
const repoInfo = JSON.parse(
|
|
execFileSync('gh', ['repo', 'view', '--json', 'nameWithOwner'], {
|
|
encoding: 'utf-8',
|
|
}),
|
|
);
|
|
repoName = repoInfo.nameWithOwner;
|
|
} catch (err) {
|
|
throw new Error(`Failed to initialize repository info: ${err}`);
|
|
}
|
|
|
|
let page = 1;
|
|
const perPage = 100;
|
|
let hasMore = true;
|
|
const maxRuns = 5000;
|
|
let totalRunsProcessed = 0;
|
|
|
|
while (hasMore && totalRunsProcessed < maxRuns) {
|
|
let output: string;
|
|
try {
|
|
output = execFileSync(
|
|
'gh',
|
|
[
|
|
'api',
|
|
`repos/${repoName}/actions/runs?created=>=${sevenDaysAgoDate}&per_page=${perPage}&page=${page}`,
|
|
],
|
|
{ encoding: 'utf-8' },
|
|
);
|
|
} catch (err) {
|
|
process.stderr.write(`Failed to fetch page ${page}: ${err}\n`);
|
|
break;
|
|
}
|
|
|
|
let workflow_runs: any[];
|
|
try {
|
|
const parsed = JSON.parse(output);
|
|
workflow_runs = parsed.workflow_runs;
|
|
} catch (err) {
|
|
process.stderr.write(`Failed to parse runs JSON: ${err}\n`);
|
|
break;
|
|
}
|
|
|
|
if (!workflow_runs || workflow_runs.length === 0) {
|
|
hasMore = false;
|
|
break;
|
|
}
|
|
|
|
const chunkSize = 20;
|
|
for (let i = 0; i < workflow_runs.length; i += chunkSize) {
|
|
const chunk = workflow_runs.slice(i, i + chunkSize);
|
|
await Promise.all(
|
|
chunk.map(async (r: { id: number; name?: string }) => {
|
|
try {
|
|
const res = await fetch(
|
|
`https://api.github.com/repos/${repoName}/actions/runs/${r.id}/jobs`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!res.ok) return;
|
|
|
|
const { jobs } = (await res.json()) as { jobs: any[] };
|
|
let runBillableMinutes = 0;
|
|
|
|
for (const job of jobs || []) {
|
|
if (!job.started_at || !job.completed_at) continue;
|
|
const start = new Date(job.started_at).getTime();
|
|
const end = new Date(job.completed_at).getTime();
|
|
const durationMs = end - start;
|
|
|
|
if (durationMs > 0) {
|
|
runBillableMinutes += Math.ceil(durationMs / (1000 * 60));
|
|
}
|
|
}
|
|
|
|
if (runBillableMinutes > 0) {
|
|
const name = r.name || 'Unknown';
|
|
workflowMinutes[name] =
|
|
(workflowMinutes[name] || 0) + runBillableMinutes;
|
|
}
|
|
} catch {
|
|
// Ignore failures for individual runs
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
totalRunsProcessed += workflow_runs.length;
|
|
if (workflow_runs.length < perPage) {
|
|
hasMore = false;
|
|
} else {
|
|
page++;
|
|
}
|
|
}
|
|
|
|
return workflowMinutes;
|
|
}
|
|
|
|
async function run() {
|
|
try {
|
|
const workflowMinutes = await getWorkflowMinutes();
|
|
let totalMinutes = 0;
|
|
|
|
for (const minutes of Object.values(workflowMinutes)) {
|
|
totalMinutes += minutes;
|
|
}
|
|
|
|
process.stdout.write(`actions_spend_minutes,${totalMinutes}\n`);
|
|
|
|
for (const [name, minutes] of Object.entries(workflowMinutes)) {
|
|
const safeName = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
process.stdout.write(
|
|
`actions_spend_minutes_workflow:${safeName},${minutes}\n`,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
run();
|