diff --git a/.github/scripts/gemini-lifecycle-manager.cjs b/.github/scripts/gemini-lifecycle-manager.cjs index 6a32beeb53..3e6399ba4a 100644 --- a/.github/scripts/gemini-lifecycle-manager.cjs +++ b/.github/scripts/gemini-lifecycle-manager.cjs @@ -26,9 +26,9 @@ module.exports = async ({ github, context, core }) => { '🗓️ Public Roadmap', ]; - const STALE_DAYS = 60; - const CLOSE_DAYS = 14; - const NO_RESPONSE_DAYS = 14; + const STALE_DAYS = 30; + const CLOSE_DAYS = 7; + const NO_RESPONSE_DAYS = 7; const now = new Date(); const staleThreshold = new Date( diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c022e2916c..881b32f08c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,7 +239,7 @@ jobs: continue-on-error: true strategy: matrix: - node-version: ${{ fromJSON(github.event_name == 'pull_request' && '["20.x"]' || '["20.x", "22.x", "24.x"]') }} + node-version: ["20.x"] shard: - 'cli' - 'others' diff --git a/tools/gemini-cli-bot/metrics/scripts/bottlenecks.ts b/tools/gemini-cli-bot/metrics/scripts/bottlenecks.ts index 241f95ca55..1b20da250a 100644 --- a/tools/gemini-cli-bot/metrics/scripts/bottlenecks.ts +++ b/tools/gemini-cli-bot/metrics/scripts/bottlenecks.ts @@ -7,9 +7,8 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; import { execSync } from 'node:child_process'; -interface IssueNode { +interface HotIssueNode { number: number; - updatedAt: string; comments: { totalCount: number; }; @@ -20,68 +19,35 @@ interface IssueNode { */ function run() { try { - const issues: IssueNode[] = []; - let hasNextPage = true; - let endCursor: string | null = null; - const MAX_ISSUES = 1000; + const now = new Date(); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - // Fetch up to 1000 open issues, sorted by least recently updated, using pagination. - while (hasNextPage && issues.length < MAX_ISSUES) { - const query = ` - query($owner: String!, $repo: String!, $after: String) { - repository(owner: $owner, name: $repo) { - issues(first: 100, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}, after: $after) { - nodes { - number - updatedAt - comments { - totalCount - } - } - pageInfo { - hasNextPage - endCursor - } - } - } + // 1. Count Zombie issues using Search API totalCount (unlimited) + const zombieSearchQuery = `is:issue is:open repo:${GITHUB_OWNER}/${GITHUB_REPO} updated:<${thirtyDaysAgo.toISOString()}`; + const zombieQuery = ` + query($searchQuery: String!) { + search(query: $searchQuery, type: ISSUE, first: 0) { + issueCount } - `; - const variables = endCursor ? `-F after=${endCursor}` : ''; - const output = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} ${variables} -f query='${query}'`, - { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }, - ).trim(); - - const data = JSON.parse(output).data.repository.issues; - issues.push(...data.nodes); - hasNextPage = data.pageInfo.hasNextPage; - endCursor = data.pageInfo.endCursor; } + `; + const zombieOutput = execSync( + `gh api graphql -F searchQuery='${zombieSearchQuery}' -f query='${zombieQuery}'`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }, + ).trim(); + const zombieCount = JSON.parse(zombieOutput).data.search.issueCount; + process.stdout.write(`bottleneck_zombie_issues_count,${zombieCount}\n`); - if (issues.length === 0) { - process.stdout.write('bottleneck_zombie_issues_count,0\n'); - return; - } - - const now = new Date().getTime(); - const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000; - - const zombies = issues.filter((issue) => { - const updated = new Date(issue.updatedAt).getTime(); - return updated < thirtyDaysAgo; - }); - - process.stdout.write(`bottleneck_zombie_issues_count,${zombies.length}\n`); - - // Also identify "Hot" issues in the same sample (though less likely to find them in the 'oldest' sample) - // But we can also fetch 'most recently updated' to find Hot issues. + // 2. Identify "Hot" issues. Since we need to count comments per issue, + // we still need to fetch some nodes, but we can target the most active ones. + const hotSearchQuery = `is:issue is:open repo:${GITHUB_OWNER}/${GITHUB_REPO} updated:>${sevenDaysAgo.toISOString()} sort:comments-desc`; const hotQuery = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - issues(last: 100, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) { - nodes { + query($searchQuery: String!) { + search(query: $searchQuery, type: ISSUE, first: 100) { + nodes { + ... on Issue { number - updatedAt comments { totalCount } @@ -91,19 +57,17 @@ function run() { } `; const hotOutput = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${hotQuery}'`, + `gh api graphql -F searchQuery='${hotSearchQuery}' -f query='${hotQuery}'`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }, ).trim(); - const hotData = JSON.parse(hotOutput).data.repository; - const hotIssues: IssueNode[] = hotData.issues.nodes; - - const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000; - const veryHot = hotIssues.filter((issue) => { - const updated = new Date(issue.updatedAt).getTime(); - return updated > sevenDaysAgo && issue.comments.totalCount > 10; - }); - + const hotNodes = JSON.parse(hotOutput).data.search.nodes as HotIssueNode[]; + + // We define "Hot" as > 10 comments in the last 7 days. + // Note: Search query 'sort:comments-desc' gets those with most total comments, + // which is a good proxy for 'Hot' when filtered by recent updates. + const veryHot = hotNodes.filter((node) => node.comments.totalCount > 10); process.stdout.write(`bottleneck_hot_issues_count,${veryHot.length}\n`); + } catch (error) { process.stderr.write( error instanceof Error ? error.message : String(error), diff --git a/tools/gemini-cli-bot/metrics/scripts/priority_distribution.ts b/tools/gemini-cli-bot/metrics/scripts/priority_distribution.ts index 7fc6349def..1ca8f6cfb0 100644 --- a/tools/gemini-cli-bot/metrics/scripts/priority_distribution.ts +++ b/tools/gemini-cli-bot/metrics/scripts/priority_distribution.ts @@ -7,92 +7,48 @@ import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; import { execSync } from 'node:child_process'; -interface IssueNode { - labels: { - nodes: Array<{ name: string }>; - }; -} - /** * Calculates the distribution of open issues across priority labels. */ function run() { try { - const issues: IssueNode[] = []; - let hasPreviousPage = true; - let startCursor: string | null = null; - const MAX_ISSUES = 1000; - - // Fetch up to 1000 open issues and their labels using pagination. - // Using 'last' to get more recent context. - while (hasPreviousPage && issues.length < MAX_ISSUES) { - const query = ` - query($owner: String!, $repo: String!, $before: String) { - repository(owner: $owner, name: $repo) { - issues(last: 100, states: OPEN, before: $before) { - nodes { - labels(first: 20) { - nodes { - name - } - } - } - pageInfo { - hasPreviousPage - startCursor - } - } - } - } - `; - const variables = startCursor ? `-F before=${startCursor}` : ''; - const output = execSync( - `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} ${variables} -f query='${query}'`, - { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }, - ).trim(); - - const data = JSON.parse(output).data.repository.issues; - issues.push(...data.nodes); - hasPreviousPage = data.pageInfo.hasPreviousPage; - startCursor = data.pageInfo.startCursor; + const repo = `${GITHUB_OWNER}/${GITHUB_REPO}`; + const query = ` + query($p0: String!, $p1: String!, $p2: String!, $p3: String!, $all: String!) { + p0: search(query: $p0, type: ISSUE, first: 0) { issueCount } + p1: search(query: $p1, type: ISSUE, first: 0) { issueCount } + p2: search(query: $p2, type: ISSUE, first: 0) { issueCount } + p3: search(query: $p3, type: ISSUE, first: 0) { issueCount } + all: search(query: $all, type: ISSUE, first: 0) { issueCount } } + `; - const distribution: Record = { - p0: 0, - p1: 0, - p2: 0, - p3: 0, - other: 0, + const variables = { + p0: `is:issue is:open repo:${repo} label:p0`, + p1: `is:issue is:open repo:${repo} label:p1`, + p2: `is:issue is:open repo:${repo} label:p2`, + p3: `is:issue is:open repo:${repo} label:p3`, + all: `is:issue is:open repo:${repo}`, }; - issues.forEach((issue) => { - let found = false; - issue.labels.nodes.forEach((label) => { - const name = label.name.toLowerCase(); - if (name.includes('p0')) { - distribution.p0++; - found = true; - } else if (name.includes('p1')) { - distribution.p1++; - found = true; - } else if (name.includes('p2')) { - distribution.p2++; - found = true; - } else if (name.includes('p3')) { - distribution.p3++; - found = true; - } - }); - if (!found) { - distribution.other++; - } - }); + const output = execSync( + `gh api graphql -F p0='${variables.p0}' -F p1='${variables.p1}' -F p2='${variables.p2}' -F p3='${variables.p3}' -F all='${variables.all}' -f query='${query}'`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }, + ).trim(); - process.stdout.write(`priority_p0_count,${distribution.p0}\n`); - process.stdout.write(`priority_p1_count,${distribution.p1}\n`); - process.stdout.write(`priority_p2_count,${distribution.p2}\n`); - process.stdout.write(`priority_p3_count,${distribution.p3}\n`); - process.stdout.write(`priority_none_count,${distribution.other}\n`); + const data = JSON.parse(output).data; + const p0Count = data.p0.issueCount; + const p1Count = data.p1.issueCount; + const p2Count = data.p2.issueCount; + const p3Count = data.p3.issueCount; + const totalOpen = data.all.issueCount; + const noneCount = totalOpen - (p0Count + p1Count + p2Count + p3Count); + + process.stdout.write(`priority_p0_count,${p0Count}\n`); + process.stdout.write(`priority_p1_count,${p1Count}\n`); + process.stdout.write(`priority_p2_count,${p2Count}\n`); + process.stdout.write(`priority_p3_count,${p3Count}\n`); + process.stdout.write(`priority_none_count,${noneCount}\n`); } catch (error) { process.stderr.write( error instanceof Error ? error.message : String(error),