Skip to content

Commit f87072f

Browse files
authored
feat(bot): add actions spend metric script (google-gemini#26463)
1 parent 6a3175e commit f87072f

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

tools/gemini-cli-bot/brain/metrics.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ synchronize with previous sessions:
4747
than closure rates).
4848
- **Proactive Opportunities**: Even if metrics are stable, identify areas where
4949
maintainability or productivity could be improved.
50+
- **Cost Savings (Lowest Priority)**: Monitor `actions_spend_minutes` and Gemini
51+
usage for significant anomalies. You may proactively recommend cost savings
52+
for both Actions and Gemini usage, provided that other repository health and
53+
latency priorities are satisfied first.
5054

5155
### 2. Hypothesis Testing & Deep Dive
5256

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { execFileSync } from 'node:child_process';
8+
9+
async function getWorkflowMinutes(): Promise<Record<string, number>> {
10+
const sevenDaysAgoDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
11+
.toISOString()
12+
.split('T')[0];
13+
14+
const output = execFileSync(
15+
'gh',
16+
[
17+
'run',
18+
'list',
19+
'--limit',
20+
'1000',
21+
'--created',
22+
`>=${sevenDaysAgoDate}`,
23+
'--json',
24+
'databaseId,workflowName',
25+
],
26+
{ encoding: 'utf-8' },
27+
);
28+
29+
const runs = JSON.parse(output);
30+
const workflowMinutes: Record<string, number> = {};
31+
const token = execFileSync('gh', ['auth', 'token'], {
32+
encoding: 'utf-8',
33+
}).trim();
34+
const repoInfo = JSON.parse(
35+
execFileSync('gh', ['repo', 'view', '--json', 'nameWithOwner'], {
36+
encoding: 'utf-8',
37+
}),
38+
);
39+
const repoName = repoInfo.nameWithOwner;
40+
41+
const chunkSize = 20;
42+
for (let i = 0; i < runs.length; i += chunkSize) {
43+
const chunk = runs.slice(i, i + chunkSize);
44+
await Promise.all(
45+
chunk.map(async (r: { databaseId: number; workflowName?: string }) => {
46+
try {
47+
const res = await fetch(
48+
`https://api.github.com/repos/${repoName}/actions/runs/${r.databaseId}/jobs`,
49+
{
50+
headers: {
51+
Authorization: `Bearer ${token}`,
52+
Accept: 'application/vnd.github.v3+json',
53+
},
54+
},
55+
);
56+
57+
if (!res.ok) return;
58+
59+
const { jobs } = await res.json();
60+
let runBillableMinutes = 0;
61+
62+
for (const job of jobs || []) {
63+
if (!job.started_at || !job.completed_at) continue;
64+
const start = new Date(job.started_at).getTime();
65+
const end = new Date(job.completed_at).getTime();
66+
const durationMs = end - start;
67+
68+
if (durationMs > 0) {
69+
runBillableMinutes += Math.ceil(durationMs / (1000 * 60));
70+
}
71+
}
72+
73+
if (runBillableMinutes > 0) {
74+
const name = r.workflowName || 'Unknown';
75+
workflowMinutes[name] =
76+
(workflowMinutes[name] || 0) + runBillableMinutes;
77+
}
78+
} catch {
79+
// Ignore failures for individual runs
80+
}
81+
}),
82+
);
83+
}
84+
85+
return workflowMinutes;
86+
}
87+
88+
async function run() {
89+
try {
90+
const workflowMinutes = await getWorkflowMinutes();
91+
let totalMinutes = 0;
92+
93+
for (const minutes of Object.values(workflowMinutes)) {
94+
totalMinutes += minutes;
95+
}
96+
97+
const now = new Date().toISOString();
98+
console.log(
99+
JSON.stringify({
100+
metric: 'actions_spend_minutes',
101+
value: totalMinutes,
102+
timestamp: now,
103+
details: workflowMinutes,
104+
}),
105+
);
106+
107+
for (const [name, minutes] of Object.entries(workflowMinutes)) {
108+
const safeName = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
109+
console.log(
110+
JSON.stringify({
111+
metric: `actions_spend_minutes_workflow:${safeName}`,
112+
value: minutes,
113+
timestamp: now,
114+
}),
115+
);
116+
}
117+
} catch (error) {
118+
process.stderr.write(
119+
error instanceof Error ? error.message : String(error),
120+
);
121+
process.exit(1);
122+
}
123+
}
124+
125+
run();

0 commit comments

Comments
 (0)