mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 21:32:56 -07:00
ci: robust stale issue lifecycle and consolidated triage bot labels
This commit is contained in:
@@ -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.`);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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: |-
|
||||
|
||||
Reference in New Issue
Block a user