diff --git a/.github/workflows/chained_e2e.yml b/.github/workflows/chained_e2e.yml index 4a5de8bf7c..5e208e7e20 100644 --- a/.github/workflows/chained_e2e.yml +++ b/.github/workflows/chained_e2e.yml @@ -184,7 +184,7 @@ jobs: needs: - 'merge_queue_skipper' - 'parse_run_context' - runs-on: 'macos-latest-large' + runs-on: 'macos-latest' if: | github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true') steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ef8bdb58d..28f682c30e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,7 +231,7 @@ jobs: test_mac: name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}' - runs-on: 'macos-latest-large' + runs-on: 'macos-latest' needs: - 'merge_queue_skipper' if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'" @@ -244,8 +244,6 @@ jobs: matrix: node-version: - '20.x' - - '22.x' - - '24.x' shard: - 'cli' - 'others' diff --git a/.github/workflows/deflake.yml b/.github/workflows/deflake.yml index a6a7d3664f..c58d9120f3 100644 --- a/.github/workflows/deflake.yml +++ b/.github/workflows/deflake.yml @@ -78,7 +78,7 @@ jobs: deflake_e2e_mac: name: 'E2E Test (macOS)' - runs-on: 'macos-latest-large' + runs-on: 'macos-latest' if: "github.repository == 'google-gemini/gemini-cli'" steps: - name: 'Checkout' diff --git a/tools/gemini-cli-bot/metrics/scripts/actions_spend.ts b/tools/gemini-cli-bot/metrics/scripts/actions_spend.ts index 5fe30852a1..070d91c4c5 100644 --- a/tools/gemini-cli-bot/metrics/scripts/actions_spend.ts +++ b/tools/gemini-cli-bot/metrics/scripts/actions_spend.ts @@ -11,22 +11,32 @@ async function getWorkflowMinutes(): Promise> { .toISOString() .split('T')[0]; - const output = execFileSync( - 'gh', - [ - 'run', - 'list', - '--limit', - '1000', - '--created', - `>=${sevenDaysAgoDate}`, - '--json', - 'databaseId,workflowName', - ], - { encoding: 'utf-8' }, - ); + let runs: any[] = []; + let page = 1; + while (true) { + const output = execFileSync( + 'gh', + [ + 'run', + 'list', + '--limit', + '1000', + '--page', + page.toString(), + '--created', + `>=${sevenDaysAgoDate}`, + '--json', + 'databaseId,workflowName', + ], + { encoding: 'utf-8' }, + ); + const pageRuns = JSON.parse(output); + if (pageRuns.length === 0) break; + runs = runs.concat(pageRuns); + if (pageRuns.length < 1000) break; + page++; + } - const runs = JSON.parse(output); const workflowMinutes: Record = {}; const token = execFileSync('gh', ['auth', 'token'], { encoding: 'utf-8', @@ -95,23 +105,12 @@ async function run() { } const now = new Date().toISOString(); - console.log( - JSON.stringify({ - metric: 'actions_spend_minutes', - value: totalMinutes, - timestamp: now, - details: workflowMinutes, - }), - ); + 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(); - console.log( - JSON.stringify({ - metric: `actions_spend_minutes_workflow:${safeName}`, - value: minutes, - timestamp: now, - }), + process.stdout.write( + `actions_spend_minutes_workflow:${safeName},${minutes}\n`, ); } } catch (error) { diff --git a/tools/gemini-cli-bot/metrics/scripts/latency.ts b/tools/gemini-cli-bot/metrics/scripts/latency.ts index 7dd5dcd1f6..6bee15c17a 100644 --- a/tools/gemini-cli-bot/metrics/scripts/latency.ts +++ b/tools/gemini-cli-bot/metrics/scripts/latency.ts @@ -10,111 +10,50 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; import { execSync } from 'node:child_process'; try { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - pullRequests(last: 100, states: MERGED) { - nodes { - authorAssociation - createdAt - mergedAt - } - } - issues(last: 100, states: CLOSED) { - nodes { - authorAssociation - createdAt - closedAt - } - } - } - } - `; - const output = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`, - { encoding: 'utf-8' }, - ); - const data = JSON.parse(output).data.repository; + const days = 7; + const sinceDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; - const prs = data.pullRequests.nodes.map( - (p: { - authorAssociation: string; - mergedAt: string; - createdAt: string; - }) => ({ - association: p.authorAssociation, - latencyHours: - (new Date(p.mergedAt).getTime() - new Date(p.createdAt).getTime()) / - (1000 * 60 * 60), - }), - ); - const issues = data.issues.nodes.map( - (i: { - authorAssociation: string; - closedAt: string; - createdAt: string; - }) => ({ - association: i.authorAssociation, - latencyHours: - (new Date(i.closedAt).getTime() - new Date(i.createdAt).getTime()) / - (1000 * 60 * 60), - }), - ); + const getItems = (type: 'prs' | 'issues') => { + const field = type === 'prs' ? 'merged-at' : 'closed-at'; + const jsonFields = type === 'prs' ? 'authorAssociation,createdAt,mergedAt' : 'authorAssociation,createdAt,closedAt'; + const output = execSync( + `gh search ${type} --repo ${GITHUB_OWNER}/${GITHUB_REPO} --${field} >=${sinceDate} --limit 1000 --json ${jsonFields}`, + { encoding: 'utf-8' } + ); + return JSON.parse(output); + }; + + const prs = getItems('prs').map((p: any) => ({ + association: p.authorAssociation, + latencyHours: (new Date(p.mergedAt).getTime() - new Date(p.createdAt).getTime()) / (1000 * 60 * 60), + })); + + const issues = getItems('issues').map((i: any) => ({ + association: i.authorAssociation, + latencyHours: (new Date(i.closedAt).getTime() - new Date(i.createdAt).getTime()) / (1000 * 60 * 60), + })); const isMaintainer = (assoc: string) => ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc); - const calculateAvg = ( - items: { association: string; latencyHours: number }[], - ) => - items.length - ? items.reduce((a, b) => a + b.latencyHours, 0) / items.length - : 0; + const calculateAvg = (items: { association: string; latencyHours: number }[]) => + items.length ? items.reduce((a, b) => a + b.latencyHours, 0) / items.length : 0; - const prMaintainers = calculateAvg( - prs.filter((i: { association: string; latencyHours: number }) => - isMaintainer(i.association), - ), - ); - const prCommunity = calculateAvg( - prs.filter( - (i: { association: string; latencyHours: number }) => - !isMaintainer(i.association), - ), - ); + const prMaintainers = calculateAvg(prs.filter((i) => isMaintainer(i.association))); + const prCommunity = calculateAvg(prs.filter((i) => !isMaintainer(i.association))); const prOverall = calculateAvg(prs); - const issueMaintainers = calculateAvg( - issues.filter((i: { association: string; latencyHours: number }) => - isMaintainer(i.association), - ), - ); - const issueCommunity = calculateAvg( - issues.filter( - (i: { association: string; latencyHours: number }) => - !isMaintainer(i.association), - ), - ); + const issueMaintainers = calculateAvg(issues.filter((i) => isMaintainer(i.association))); + const issueCommunity = calculateAvg(issues.filter((i) => !isMaintainer(i.association))); const issueOverall = calculateAvg(issues); - process.stdout.write( - `latency_pr_overall_hours,${Math.round(prOverall * 100) / 100}\n`, - ); - process.stdout.write( - `latency_pr_maintainers_hours,${Math.round(prMaintainers * 100) / 100}\n`, - ); - process.stdout.write( - `latency_pr_community_hours,${Math.round(prCommunity * 100) / 100}\n`, - ); - process.stdout.write( - `latency_issue_overall_hours,${Math.round(issueOverall * 100) / 100}\n`, - ); - process.stdout.write( - `latency_issue_maintainers_hours,${Math.round(issueMaintainers * 100) / 100}\n`, - ); - process.stdout.write( - `latency_issue_community_hours,${Math.round(issueCommunity * 100) / 100}\n`, - ); + process.stdout.write(`latency_pr_overall_hours,${Math.round(prOverall * 100) / 100}\n`); + process.stdout.write(`latency_pr_maintainers_hours,${Math.round(prMaintainers * 100) / 100}\n`); + process.stdout.write(`latency_pr_community_hours,${Math.round(prCommunity * 100) / 100}\n`); + process.stdout.write(`latency_issue_overall_hours,${Math.round(issueOverall * 100) / 100}\n`); + process.stdout.write(`latency_issue_maintainers_hours,${Math.round(issueMaintainers * 100) / 100}\n`); + process.stdout.write(`latency_issue_community_hours,${Math.round(issueCommunity * 100) / 100}\n`); } catch (err) { process.stderr.write(err instanceof Error ? err.message : String(err)); process.exit(1); } + diff --git a/tools/gemini-cli-bot/metrics/scripts/throughput.ts b/tools/gemini-cli-bot/metrics/scripts/throughput.ts index 3806dd407a..10f650fbe6 100644 --- a/tools/gemini-cli-bot/metrics/scripts/throughput.ts +++ b/tools/gemini-cli-bot/metrics/scripts/throughput.ts @@ -10,111 +10,61 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; import { execSync } from 'node:child_process'; try { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - pullRequests(last: 100, states: MERGED) { - nodes { - authorAssociation - mergedAt - } - } - issues(last: 100, states: CLOSED) { - nodes { - authorAssociation - closedAt - } - } - } - } - `; - const output = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`, - { encoding: 'utf-8' }, - ); - const data = JSON.parse(output).data.repository; + const days = 7; + const sinceDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + const getItems = (type: 'prs' | 'issues') => { + const field = type === 'prs' ? 'merged-at' : 'closed-at'; + const jsonFields = type === 'prs' ? 'authorAssociation,mergedAt' : 'authorAssociation,closedAt'; + const output = execSync( + `gh search ${type} --repo ${GITHUB_OWNER}/${GITHUB_REPO} --${field} >=${sinceDate} --limit 1000 --json ${jsonFields}`, + { encoding: 'utf-8' } + ); + return JSON.parse(output); + }; - const prs = data.pullRequests.nodes - .map((p: { authorAssociation: string; mergedAt: string }) => ({ - association: p.authorAssociation, - date: new Date(p.mergedAt).getTime(), - })) - .sort((a: { date: number }, b: { date: number }) => a.date - b.date); + const prs = getItems('prs').map((p: any) => ({ + association: p.authorAssociation, + date: new Date(p.mergedAt).getTime(), + })); - const issues = data.issues.nodes - .map((i: { authorAssociation: string; closedAt: string }) => ({ - association: i.authorAssociation, - date: new Date(i.closedAt).getTime(), - })) - .sort((a: { date: number }, b: { date: number }) => a.date - b.date); + const issues = getItems('issues').map((i: any) => ({ + association: i.authorAssociation, + date: new Date(i.closedAt).getTime(), + })); const isMaintainer = (assoc: string) => ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc); - const calculateThroughput = ( - items: { association: string; date: number }[], - ) => { - if (items.length < 2) return 0; - const first = items[0].date; - const last = items[items.length - 1].date; - const days = (last - first) / (1000 * 60 * 60 * 24); - return days > 0 ? items.length / days : items.length; // items per day - }; + const calculateThroughput = (items: any[]) => items.length / days; const prOverall = calculateThroughput(prs); const prMaintainers = calculateThroughput( - prs.filter((i: { association: string; date: number }) => - isMaintainer(i.association), - ), + prs.filter((i) => isMaintainer(i.association)) ); const prCommunity = calculateThroughput( - prs.filter( - (i: { association: string; date: number }) => - !isMaintainer(i.association), - ), + prs.filter((i) => !isMaintainer(i.association)) ); const issueOverall = calculateThroughput(issues); const issueMaintainers = calculateThroughput( - issues.filter((i: { association: string; date: number }) => - isMaintainer(i.association), - ), + issues.filter((i) => isMaintainer(i.association)) ); const issueCommunity = calculateThroughput( - issues.filter( - (i: { association: string; date: number }) => - !isMaintainer(i.association), - ), + issues.filter((i) => !isMaintainer(i.association)) ); - process.stdout.write( - `throughput_pr_overall_per_day,${Math.round(prOverall * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_pr_maintainers_per_day,${Math.round(prMaintainers * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_pr_community_per_day,${Math.round(prCommunity * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_issue_overall_per_day,${Math.round(issueOverall * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_issue_maintainers_per_day,${Math.round(issueMaintainers * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_issue_community_per_day,${Math.round(issueCommunity * 100) / 100}\n`, - ); - process.stdout.write( - `throughput_issue_overall_days_per_issue,${issueOverall > 0 ? Math.round((1 / issueOverall) * 100) / 100 : 0}\n`, - ); - process.stdout.write( - `throughput_issue_maintainers_days_per_issue,${issueMaintainers > 0 ? Math.round((1 / issueMaintainers) * 100) / 100 : 0}\n`, - ); - process.stdout.write( - `throughput_issue_community_days_per_issue,${issueCommunity > 0 ? Math.round((1 / issueCommunity) * 100) / 100 : 0}\n`, - ); + process.stdout.write(`throughput_pr_overall_per_day,${Math.round(prOverall * 100) / 100}\n`); + process.stdout.write(`throughput_pr_maintainers_per_day,${Math.round(prMaintainers * 100) / 100}\n`); + process.stdout.write(`throughput_pr_community_per_day,${Math.round(prCommunity * 100) / 100}\n`); + process.stdout.write(`throughput_issue_overall_per_day,${Math.round(issueOverall * 100) / 100}\n`); + process.stdout.write(`throughput_issue_maintainers_per_day,${Math.round(issueMaintainers * 100) / 100}\n`); + process.stdout.write(`throughput_issue_community_per_day,${Math.round(issueCommunity * 100) / 100}\n`); + process.stdout.write(`throughput_issue_overall_days_per_issue,${issueOverall > 0 ? Math.round((1 / issueOverall) * 100) / 100 : 0}\n`); + process.stdout.write(`throughput_issue_maintainers_days_per_issue,${issueMaintainers > 0 ? Math.round((1 / issueMaintainers) * 100) / 100 : 0}\n`); + process.stdout.write(`throughput_issue_community_days_per_issue,${issueCommunity > 0 ? Math.round((1 / issueCommunity) * 100) / 100 : 0}\n`); } catch (err) { process.stderr.write(err instanceof Error ? err.message : String(err)); process.exit(1); } + diff --git a/tools/gemini-cli-bot/metrics/scripts/user_touches.ts b/tools/gemini-cli-bot/metrics/scripts/user_touches.ts index 5ccffa94fc..e650600668 100644 --- a/tools/gemini-cli-bot/metrics/scripts/user_touches.ts +++ b/tools/gemini-cli-bot/metrics/scripts/user_touches.ts @@ -10,52 +10,30 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; import { execSync } from 'node:child_process'; try { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - pullRequests(last: 100, states: MERGED) { - nodes { - authorAssociation - comments { totalCount } - reviews { totalCount } - } - } - issues(last: 100, states: CLOSED) { - nodes { - authorAssociation - comments { totalCount } - } - } - } - } - `; - const output = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`, - { encoding: 'utf-8' }, - ); - const data = JSON.parse(output).data.repository; + const days = 7; + const sinceDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; - const prs = data.pullRequests.nodes; - const issues = data.issues.nodes; + const getItems = (type: 'prs' | 'issues') => { + const field = type === 'prs' ? 'merged-at' : 'closed-at'; + const jsonFields = type === 'prs' ? 'authorAssociation,comments,reviews' : 'authorAssociation,comments'; + const output = execSync( + `gh search ${type} --repo ${GITHUB_OWNER}/${GITHUB_REPO} --${field} >=${sinceDate} --limit 1000 --json ${jsonFields}`, + { encoding: 'utf-8' } + ); + return JSON.parse(output); + }; - const allItems = [ - ...prs.map( - (p: { - authorAssociation: string; - comments: { totalCount: number }; - reviews?: { totalCount: number }; - }) => ({ - association: p.authorAssociation, - touches: p.comments.totalCount + (p.reviews ? p.reviews.totalCount : 0), - }), - ), - ...issues.map( - (i: { authorAssociation: string; comments: { totalCount: number } }) => ({ - association: i.authorAssociation, - touches: i.comments.totalCount, - }), - ), - ]; + const prs = getItems('prs').map((p: any) => ({ + association: p.authorAssociation, + touches: (p.comments?.length || 0) + (p.reviews?.length || 0), + })); + + const issues = getItems('issues').map((i: any) => ({ + association: i.authorAssociation, + touches: i.comments?.length || 0, + })); + + const allItems = [...prs, ...issues]; const isMaintainer = (assoc: string) => ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc); @@ -84,3 +62,4 @@ try { process.stderr.write(err instanceof Error ? err.message : String(err)); process.exit(1); } +