diff --git a/.github/scripts/cleanup-triage-labels.cjs b/.github/scripts/cleanup-triage-labels.cjs index 5b492bb423..bd433918fb 100644 --- a/.github/scripts/cleanup-triage-labels.cjs +++ b/.github/scripts/cleanup-triage-labels.cjs @@ -22,29 +22,39 @@ module.exports = async ({ github, context, core }) => { for (const issue of issuesToCleanup) { try { - await github.rest.issues.removeLabel({ + const { data: issueData } = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - name: 'status/need-triage', }); - core.info( - `Successfully removed status/need-triage from #${issue.number}`, + + const labels = issueData.labels.map((l) => + typeof l === 'string' ? l : l.name, ); - } catch (error) { - if (error.status === 404) { - core.info( - `Label status/need-triage not found on #${issue.number}, skipping.`, - ); - } else { - core.warning( - `Failed to remove label from #${issue.number}: ${error.message}`, - ); + + if (labels.includes('status/bot-triaged') && labels.includes('status/need-triage')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: 'status/need-triage', + }); + core.info(`Successfully removed status/need-triage from #${issue.number}`); } + + if (labels.includes('status/bot-triaged') && labels.includes('status/manual-triage')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: 'status/bot-triaged', + }); + core.info(`Successfully removed status/bot-triaged from #${issue.number} because it requires manual triage`); + } + } catch (error) { + core.warning(`Failed to clean up labels for #${issue.number}: ${error.message}`); } } - core.info( - `Cleaned up status/need-triage from ${issuesToCleanup.length} issues.`, - ); + core.info(`Cleaned up conflicting labels from ${issuesToCleanup.length} issues.`); }; diff --git a/.github/scripts/gemini-lifecycle-manager.cjs b/.github/scripts/gemini-lifecycle-manager.cjs index a5d5b13e0e..4902e527cc 100644 --- a/.github/scripts/gemini-lifecycle-manager.cjs +++ b/.github/scripts/gemini-lifecycle-manager.cjs @@ -79,14 +79,13 @@ module.exports = async ({ github, context, core }) => { async function processItems(query, callback) { core.info(`Searching: ${query}`); try { - const response = await github.rest.search.issuesAndPullRequests({ + const items = await github.paginate(github.rest.search.issuesAndPullRequests, { q: query, per_page: 100, sort: 'updated', order: 'asc', }); - const items = response.data.items; - core.info(`Found ${items.length} items (batch limited).`); + core.info(`Found ${items.length} items.`); for (const item of items) { try { await callback(item); @@ -114,10 +113,11 @@ module.exports = async ({ github, context, core }) => { per_page: 5, }); - // Check if the last comment is from a non-maintainer + // Check if the last comment is from a non-maintainer and not a bot const lastComment = comments[0]; if ( lastComment && + lastComment.user?.type !== 'Bot' && !(await isMaintainer(lastComment.user, lastComment.author_association)) ) { core.info( @@ -156,6 +156,7 @@ module.exports = async ({ github, context, core }) => { repo, issue_number: item.number, state: 'closed', + state_reason: 'not_planned', }); } }, @@ -163,10 +164,16 @@ module.exports = async ({ github, context, core }) => { // 2. Handle Stale Mark (60 days inactivity, no stale label) const exemptQuery = EXEMPT_LABELS.map((l) => `-label:"${l}"`).join(' '); + await processItems( `repo:${owner}/${repo} is:open -label:"${STALE_LABEL}" ${exemptQuery} updated:<${staleThreshold.toISOString()}`, async (item) => { core.info(`Marking #${item.number} as stale.`); + const isBug = item.labels.some(l => (typeof l === 'string' ? l : l.name).toLowerCase().includes('bug')); + const bodyText = isBug + ? `This bug report has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. Many issues are resolved in newer releases. Please verify if the issue persists in the latest Gemini CLI version. If it does, please leave a comment to keep this open. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!` + : `This item has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`; + if (!dryRun) { await github.rest.issues.addLabels({ owner, @@ -178,16 +185,74 @@ module.exports = async ({ github, context, core }) => { owner, repo, issue_number: item.number, - body: `This item has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`, + body: bodyText, }); } }, ); - // 3. Handle Stale Close (14 days with stale label) + // 3. Handle Stale Removal & Close await processItems( - `repo:${owner}/${repo} is:open label:"${STALE_LABEL}" ${exemptQuery} updated:<${closeThreshold.toISOString()}`, + `repo:${owner}/${repo} is:open label:"${STALE_LABEL}" ${exemptQuery}`, async (item) => { + // Fetch full timeline to see events and comments + const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { + owner, + repo, + issue_number: item.number, + per_page: 100, + }); + + // Find exactly when the Stale label was added + // We look for the last 'labeled' event for STALE_LABEL + const staleEventIndex = timeline.findLastIndex(e => e.event === 'labeled' && e.label?.name?.toLowerCase() === STALE_LABEL.toLowerCase()); + + if (staleEventIndex === -1) return; // Fallback if no event found + + const staleEvent = timeline[staleEventIndex]; + const eventsAfterStale = timeline.slice(staleEventIndex + 1); + + // Check for meaningful activity after the Stale label was applied + const meaningfulEvents = eventsAfterStale.filter(e => { + const actor = e.actor?.login || ''; + const isBot = actor.includes('[bot]') || actor.includes('github-actions'); + + // Explicit whitelist of meaningful events + if (['commented', 'cross-referenced', 'connected', 'reopened', 'assigned'].includes(e.event)) { + // If a human commented, or ANYONE (even a bot) linked a PR, it's meaningful + if (e.event === 'commented' && isBot) return false; + return true; + } + + // If a human explicitly added or removed a label, it's meaningful + if (['labeled', 'unlabeled'].includes(e.event) && !isBot) { + return true; + } + + return false; + }); + + if (meaningfulEvents.length > 0) { + // Activity detected, remove Stale label + core.info(`Removing ${STALE_LABEL} from #${item.number} due to meaningful activity (e.g., comment or PR).`); + if (!dryRun) { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: item.number, + name: STALE_LABEL, + }).catch(() => {}); + } + return; + } + + // No meaningful activity. Check if 14 days have passed. + const labeledDate = new Date(staleEvent.created_at); + if (labeledDate > closeThreshold) { + // Has not been 14 days since it was ACTUALLY marked stale + return; + } + core.info(`Closing stale item #${item.number}.`); if (!dryRun) { await github.rest.issues.createComment({ @@ -201,6 +266,7 @@ module.exports = async ({ github, context, core }) => { repo, issue_number: item.number, state: 'closed', + state_reason: 'not_planned', }); } }, diff --git a/.github/workflows/gemini-scheduled-issue-triage.yml b/.github/workflows/gemini-scheduled-issue-triage.yml index 363c8ca3c0..2597956bd7 100644 --- a/.github/workflows/gemini-scheduled-issue-triage.yml +++ b/.github/workflows/gemini-scheduled-issue-triage.yml @@ -81,15 +81,15 @@ jobs: echo '๐Ÿ” Finding issues missing area labels...' gh issue list --repo "${GITHUB_REPOSITORY}" \ - --search 'is:open is:issue -label:status/bot-triaged -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 50 --json number,title,body > no_area_issues.json + --search 'is:open is:issue -label:status/bot-triaged -label:status/manual-triage -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 50 --json number,title,body > no_area_issues.json echo '๐Ÿ” Finding issues missing kind labels...' gh issue list --repo "${GITHUB_REPOSITORY}" \ - --search 'is:open is:issue -label:status/bot-triaged -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 50 --json number,title,body > no_kind_issues.json + --search 'is:open is:issue -label:status/bot-triaged -label:status/manual-triage -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 50 --json number,title,body > no_kind_issues.json echo '๐Ÿท๏ธ Finding issues missing priority labels...' gh issue list --repo "${GITHUB_REPOSITORY}" \ - --search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 50 --json number,title,body > no_priority_issues.json + --search 'is:open is:issue -label:status/bot-triaged -label:status/manual-triage -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 50 --json number,title,body > no_priority_issues.json echo '๐Ÿ“ Finding issues missing effort labels...' gh issue list --repo "${GITHUB_REPOSITORY}" \ @@ -213,9 +213,9 @@ jobs: ] ``` If an issue cannot be classified, do not include it in the output array. - 9. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5 - - Anything more than 6 versions older than the most recent should add the status/need-retesting label - 10. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below. + 9. For each issue, carefully check if the CLI version is present. It is usually found under the "### Client information" header, as a bullet point (e.g., "โ€ข CLI Version: 0.33.1"), or in the output of the `/about` command. + - If the version is provided but is more than 6 minor versions older than the most recent release, apply the status/need-information label and leave a comment politely asking the user to verify if the issue persists in the latest version. + 10. If the issue does not have sufficient information, recommend the status/need-information label and leave a comment politely requesting the missing details. For example, if repro steps are missing, ask for them; if the CLI version is completely missing, ask for the version information in the explanation section below. Do not ask for version info if it is already in the issue body. 11. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation. 12. If you are uncertain about a category, use the area/unknown, kind/question, or priority/unknown labels as appropriate. If you are extremely uncertain, apply the status/manual-triage label. @@ -428,9 +428,15 @@ jobs: GITHUB_REPOSITORY: '${{ github.repository }}' run: |- set -euo pipefail - echo '๐Ÿงน Finding issues that have both bot-triaged and need-triage labels...' + echo '๐Ÿงน Finding issues with conflicting triage status labels...' gh issue list --repo "${GITHUB_REPOSITORY}" \ - --search 'is:open is:issue label:status/bot-triaged label:status/need-triage' --limit 50 --json number > issues_to_cleanup.json + --search 'is:open is:issue label:status/bot-triaged label:status/need-triage' --limit 50 --json number > need_triage_cleanup.json + gh issue list --repo "${GITHUB_REPOSITORY}" \ + --search 'is:open is:issue label:status/bot-triaged label:status/manual-triage' --limit 50 --json number > manual_triage_cleanup.json + + if [ ! -f need_triage_cleanup.json ]; then echo "[]" > need_triage_cleanup.json; fi + if [ ! -f manual_triage_cleanup.json ]; then echo "[]" > manual_triage_cleanup.json; fi + jq -c -s 'add | unique_by(.number)' need_triage_cleanup.json manual_triage_cleanup.json > issues_to_cleanup.json - name: 'Clean Up Triage Labels' if: |-