From 3eab0513cd89cb64df8e8fd3282f9712d72cb37c Mon Sep 17 00:00:00 2001 From: "gemini-cli[bot]" Date: Thu, 30 Apr 2026 23:35:05 +0000 Subject: [PATCH] # Proactive Improvement: Backlog Health & Stale Policy Optimization ## Overview This PR addresses a significant growth in the repository's open issues (2342) and PRs (440) by optimizing the automated stale policy and adding visibility into backlog health. ## Changes 1. **New Metrics**: - `backlog_health.ts`: Tracks the median age (in days) of the 100 oldest open PRs and issues. This provides a "worst-case" signal for backlog stagnation. - `stale_ratio.ts`: Tracks the percentage of open items currently labeled as `stale`. 2. **Stale Policy Optimization**: - Increased `operations-per-run` in `.github/workflows/stale.yml` from default (~30) to 500 total (300 for issues, 200 for PRs). - Split the stale job into two parallel jobs (`stale-issues` and `stale-prs`) to increase daily throughput and prevent issues from blocking PR processing. ## Rationale Metrics analysis showed that while the repository has excellent "Fast Path" performance (PRs merged in ~23 hours), it has a massive "Slow Path" backlog that is likely not being touched by automation due to default throttling in `actions/stale`. By increasing the processing limit, we can accelerate the cleanup of stale items and help maintainers focus on active work. ## Impact - **Productivity**: Reduces "noise" in the issue tracker and PR list. - **Observability**: New metrics will allow the "Bot Brain" to monitor the effectiveness of these policy changes over time. - **Latency**: Expected to decrease the median age of open items as stale ones are closed. --- .github/workflows/stale.yml | 48 +++++----- .../metrics/scripts/backlog_health.ts | 94 +++++++++++++++++++ .../metrics/scripts/stale_ratio.ts | 51 ++++++++++ 3 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 tools/gemini-cli-bot/metrics/scripts/backlog_health.ts create mode 100644 tools/gemini-cli-bot/metrics/scripts/stale_ratio.ts diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4a975869f5..7bd6ee29f4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,38 +7,44 @@ on: workflow_dispatch: jobs: - stale: - strategy: - fail-fast: false - matrix: - runner: - - 'ubuntu-latest' # GitHub-hosted - runs-on: '${{ matrix.runner }}' - if: |- - ${{ github.repository == 'google-gemini/gemini-cli' }} + stale-issues: + runs-on: ubuntu-latest + if: github.repository == 'google-gemini/gemini-cli' permissions: - issues: 'write' - pull-requests: 'write' - concurrency: - group: '${{ github.workflow }}-stale' - cancel-in-progress: true + issues: write steps: - - uses: 'actions/stale@5bef64f19d7facfb25b37b414482c7164d639639' # ratchet:actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # ratchet:actions/stale@v9 with: - repo-token: '${{ secrets.GITHUB_TOKEN }}' + repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: >- This issue has been automatically marked as stale due to 60 days of inactivity. It will be closed in 14 days if no further activity occurs. - stale-pr-message: >- - This pull request has been automatically marked as stale due to 60 days of inactivity. - It will be closed in 14 days if no further activity occurs. close-issue-message: >- This issue has been closed due to 14 additional days of inactivity after being marked as stale. If you believe this is still relevant, feel free to comment or reopen the issue. Thank you! + days-before-stale: 60 + days-before-close: 14 + exempt-issue-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap,needs-triage,waiting-on-maintainer,status: blocked' + operations-per-run: 300 + only-issues: true + + stale-prs: + runs-on: ubuntu-latest + if: github.repository == 'google-gemini/gemini-cli' + permissions: + pull-requests: write + steps: + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # ratchet:actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-pr-message: >- + This pull request has been automatically marked as stale due to 60 days of inactivity. + It will be closed in 14 days if no further activity occurs. close-pr-message: >- This pull request has been closed due to 14 additional days of inactivity after being marked as stale. If this is still relevant, you are welcome to reopen or leave a comment. Thanks for contributing! days-before-stale: 60 days-before-close: 14 - exempt-issue-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap' - exempt-pr-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap' + exempt-pr-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap,needs-triage,waiting-on-maintainer,status: blocked' + operations-per-run: 200 + only-prs: true diff --git a/tools/gemini-cli-bot/metrics/scripts/backlog_health.ts b/tools/gemini-cli-bot/metrics/scripts/backlog_health.ts new file mode 100644 index 0000000000..f7abcc9c4f --- /dev/null +++ b/tools/gemini-cli-bot/metrics/scripts/backlog_health.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GITHUB_OWNER, GITHUB_REPO } from '../types.js'; +import { execSync } from 'node:child_process'; + +try { + const query = ` + query($owner: String!, $repo: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: ASC}, after: $cursor) { + totalCount + nodes { + createdAt + } + pageInfo { + hasNextPage + endCursor + } + } + issues(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: ASC}, after: $cursor) { + totalCount + nodes { + createdAt + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `; + + const fetchNodes = async (type: 'pullRequests' | 'issues') => { + let allNodes: { createdAt: string }[] = []; + let cursor: string | null = null; + let totalCount = 0; + + // Fetch up to 500 items for a reasonable median calculation + for (let i = 0; i < 5; i++) { + const output = execSync( + `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} ${cursor ? `-F cursor=${cursor}` : ''} -f query='${query}'`, + { encoding: 'utf-8' }, + ); + const result = JSON.parse(output).data.repository[type]; + totalCount = result.totalCount; + allNodes.push(...result.nodes); + if (!result.pageInfo.hasNextPage) break; + cursor = result.pageInfo.endCursor; + } + return { nodes: allNodes, totalCount }; + }; + + const { nodes: prNodes, totalCount: prTotal } = await fetchNodes('pullRequests'); + const { nodes: issueNodes, totalCount: issueTotal } = await fetchNodes('issues'); + + const now = new Date().getTime(); + + const getMedianAgeDays = (nodes: { createdAt: string }[]) => { + if (nodes.length === 0) return 0; + const ages = nodes.map( + (n) => (now - new Date(n.createdAt).getTime()) / (1000 * 60 * 60 * 24), + ); + ages.sort((a, b) => a - b); + const mid = Math.floor(ages.length / 2); + return ages.length % 2 !== 0 + ? ages[mid] + : (ages[mid - 1] + ages[mid]) / 2; + }; + + const prMedianAge = getMedianAgeDays(prNodes); + const issueMedianAge = getMedianAgeDays(issueNodes); + + process.stdout.write( + `backlog_median_age_pr_days,${Math.round(prMedianAge * 100) / 100}\n`, + ); + process.stdout.write( + `backlog_median_age_issue_days,${Math.round(issueMedianAge * 100) / 100}\n`, + ); + + if (prTotal > prNodes.length) { + process.stderr.write(`Warning: PR median based on oldest ${prNodes.length} of ${prTotal} items\n`); + } + if (issueTotal > issueNodes.length) { + process.stderr.write(`Warning: Issue median based on oldest ${issueNodes.length} of ${issueTotal} items\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/stale_ratio.ts b/tools/gemini-cli-bot/metrics/scripts/stale_ratio.ts new file mode 100644 index 0000000000..32e497c61c --- /dev/null +++ b/tools/gemini-cli-bot/metrics/scripts/stale_ratio.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +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) { + issues(states: OPEN) { + totalCount + } + pullRequests(states: OPEN) { + totalCount + } + } + staleIssues: search(query: "repo:${GITHUB_OWNER}/${GITHUB_REPO} is:issue is:open label:stale OR label:Stale", type: ISSUE, first: 0) { + issueCount + } + stalePRs: search(query: "repo:${GITHUB_OWNER}/${GITHUB_REPO} is:pr is:open label:stale OR label:Stale", type: ISSUE, first: 0) { + issueCount + } + } + `; + const output = execSync( + `gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`, + { encoding: 'utf-8' }, + ); + const json = JSON.parse(output); + const data = json.data; + + const totalIssues = data.repository.issues.totalCount; + const totalPRs = data.repository.pullRequests.totalCount; + const staleIssues = data.staleIssues.issueCount; + const stalePRs = data.stalePRs.issueCount; + + const issueRatio = totalIssues > 0 ? staleIssues / totalIssues : 0; + const prRatio = totalPRs > 0 ? stalePRs / totalPRs : 0; + + process.stdout.write( + `stale_ratio_issue,${Math.round(issueRatio * 100) / 100}\n`, + ); + process.stdout.write(`stale_ratio_pr,${Math.round(prRatio * 100) / 100}\n`); +} catch (err) { + process.stderr.write(err instanceof Error ? err.message : String(err)); + process.exit(1); +}