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); +}