mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 14:23:02 -07:00
feat(skills): add github-issue-triage skill
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: github-issue-triage
|
||||
description: Analyzes and cleans up GitHub issues. DO NOT trigger this skill automatically. ONLY use when the user explicitly mentions "github-issue-triage", or explicitly asks to "triage issues", "clean up old issues", or "triage this issue".
|
||||
---
|
||||
|
||||
# GitHub Issue Triage
|
||||
|
||||
This skill provides workflows for finding, analyzing, and triaging GitHub issues to maintain a clean and actionable backlog.
|
||||
|
||||
## Phase 1: Discovery (Optional)
|
||||
|
||||
If the user asks you to "triage issues" or "clean up old issues" without providing a specific issue URL, you must first find candidate issues.
|
||||
|
||||
Run the following script to get a list of issues:
|
||||
`node scripts/find_issues.cjs <owner/repo>` (e.g., `node scripts/find_issues.cjs google-gemini/gemini-cli`)
|
||||
|
||||
You may optionally pass a custom search string and limit.
|
||||
`node scripts/find_issues.cjs <owner/repo> "<search_string>" <limit>`
|
||||
|
||||
Pick the first issue from the list to triage and proceed to Phase 2. If the user provided a specific issue URL, start at Phase 2 directly.
|
||||
|
||||
## Phase 2: Analysis
|
||||
|
||||
For the target issue, you must run the analysis script to gather metadata and determine staleness/inactivity heuristics.
|
||||
|
||||
Run:
|
||||
`node scripts/analyze_issue.cjs <issue_url> "<optional_comma_separated_maintainers>"`
|
||||
|
||||
Read the JSON output carefully.
|
||||
- If `is_stale` is `true`, the issue has already been marked as stale and should be closed according to the rules in Phase 3.
|
||||
- Take note of `inactive_over_30_days`, `inactive_over_60_days`, `is_epic`, and other boolean flags.
|
||||
|
||||
## Phase 3: Triage Execution
|
||||
|
||||
After analyzing the issue and receiving the JSON output, you MUST consult the detailed triage rules to determine the next steps.
|
||||
|
||||
Read the rules in [references/triage_rules.md](references/triage_rules.md) and execute the appropriate steps. You must follow the steps sequentially.
|
||||
|
||||
If a step instructs you to **STOP EXECUTION**, you must conclude your work on this issue and not proceed to subsequent steps. If you are triaging a batch of issues, you may move on to the next issue in the list.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Issue Triage Rules
|
||||
|
||||
When executing triage on an issue, you must evaluate the following steps sequentially using the data provided from `scripts/analyze_issue.cjs` and the issue comments (`gh issue view <issue_url> --json comments`).
|
||||
|
||||
## Step 0: Maintainer-Only Bypass
|
||||
Check the `labels` array in the JSON output from `analyze_issue.cjs`.
|
||||
- If the issue already has a maintainer-only label (e.g., `🔒 maintainer only`): run `gh issue edit <issue_url> --remove-label "status/need-triage"` and **STOP EXECUTION**. Do not evaluate any further steps.
|
||||
|
||||
## Step 1: Resolution Check
|
||||
Read ALL the comments from the issue carefully, and review the JSON output for `cross_references`.
|
||||
Check if ANY of the following conditions are met:
|
||||
1. Is there a cross-referenced PR in `cross_references` where `is_pr` is `true` and `is_merged` is `true`?
|
||||
2. Is it fixed, resolved, no longer reproducible, or functioning properly in the comments? (e.g., someone mentions it `might be fixed`, `should be fixed`, or `no longer an issue`) AND the reporter has not replied afterward to contradict this?
|
||||
3. Is there a workaround provided in the comments?
|
||||
4. Does someone state the issue is actually unrelated to this repository/project, or is an external problem (like a terminal emulator bug)?
|
||||
|
||||
- If condition 1 is met: Execute `gh issue close <issue_url> --comment "Closing because this issue was referenced by a merged pull request. Feel free to reopen if the problem persists or if the PR did not fully resolve this." --reason "completed"` and **STOP EXECUTION**.
|
||||
- If condition 2 or 3 is met: Execute `gh issue close <issue_url> --comment "Closing because the comments indicate this issue might be fixed, has a workaround, is no longer an issue, or is resolved. Feel free to reopen if the problem persists." --reason "completed"` and **STOP EXECUTION**.
|
||||
- If condition 4 is met: Execute `gh issue close <issue_url> --comment "Closing because the comments indicate this issue is unrelated to this project or is an external problem." --reason "not planned"` and **STOP EXECUTION**.
|
||||
- If NONE of these conditions are met: Proceed to Step 1.2.
|
||||
|
||||
## Step 1.2: Closed PR Re-evaluation
|
||||
If there is a cross-referenced PR in `cross_references` where `is_pr` is `true` and `is_merged` is `false` (and its state is `closed` or it has an automated closure comment):
|
||||
1. Use `gh pr view <pr_url> --json author,comments,state,title,body` to analyze the PR.
|
||||
2. Check the comments to see if it was closed by an automated bot (e.g., `gemini-cli` bot closing it automatically due to missing labels like 'help wanted' after 14 days).
|
||||
3. Analyze the PR's title, body, and comments to determine if it implements a valid and useful feature/fix and is worth resuming.
|
||||
- If it is worth resuming AND was closed by a bot:
|
||||
a. Reopen the PR: `gh pr reopen <pr_url>`
|
||||
b. Assign the PR to the author: `gh pr edit <pr_url> --add-assignee <author_username>`
|
||||
c. Assign the issue to the author: `gh issue edit <issue_url> --add-assignee <author_username>`
|
||||
d. Add the help wanted label to the issue to prevent the bot from closing the PR again: `gh issue edit <issue_url> --add-label "help wanted"`
|
||||
e. Comment on the issue: `gh issue comment <issue_url> --body "@<author_username>, apologies! It looks like your PR <pr_url> was incorrectly closed by our bot. I have reopened it and assigned this issue to you. Would you like to continue working on it?"`
|
||||
f. Comment on the PR: `gh pr comment <pr_url> --body "@<author_username>, apologies for the bot closing this PR! We have reopened it. Please sync your branch to the latest \`main\` and we will have someone review it shortly."`
|
||||
g. Execute `gh issue edit <issue_url> --remove-label "status/need-triage"`
|
||||
h. **STOP EXECUTION**.
|
||||
- If NOT met: Proceed to Step 1.5.
|
||||
|
||||
## Step 1.5: Pending Response Check
|
||||
Read ALL the comments from the issue carefully.
|
||||
1. Check if the most recent comments include a request for more information, clarification, or reproduction steps directed at the reporter from any other user (maintainer or community member).
|
||||
2. Check if the reporter has NOT replied to that request.
|
||||
3. Check if that request was made over 14 days ago. (You can check the date of the comment vs today's date).
|
||||
|
||||
- If ALL of these conditions are met: Execute `gh issue close <issue_url> --comment "Closing because more information was requested over 2 weeks ago and we haven't received a response. Feel free to reopen if you can provide the requested details." --reason "not planned"` and **STOP EXECUTION**.
|
||||
- If NOT met: Proceed to Step 2.
|
||||
|
||||
## Step 2: Assignee and Inactivity Handling
|
||||
Use the JSON output from `analyze_issue.cjs` to determine necessary actions.
|
||||
|
||||
1. **Assignee Check:** If an assignee is a contributor and hasn't made any updates on the issue for over 2 weeks, execute `gh issue edit <issue_url> --remove-assignee <username>` to remove them. (Do this before proceeding further).
|
||||
2. **Inactivity Check:**
|
||||
- If `inactive_over_60_days` is `true`:
|
||||
a. Formulate a comment to the reporter (@<reporter_username>). Evaluate the issue description and whether it is an Epic (`is_epic`):
|
||||
- Always mention that the issue is being closed or pinged because it has been inactive for over 60 days.
|
||||
- IF IT IS AN EPIC: Ask the reporter if it is still in progress or complete, and if it is complete, ask them to close it.
|
||||
- IF IT IS NOT AN EPIC:
|
||||
- If it's a feature/enhancement request and the description is relatively vague, ask: 1) if it is still needed and 2) if they can provide more details on the feature request.
|
||||
- If the issue was mentioned by another issue that is closed as completed or by a pull request that is merged/closed (check `cross_references`), mention this cross-reference (e.g., "I see this issue was mentioned by #123 which is closed as completed...") and ask if this means it is resolved. Do NOT mention cross-references that are still open or closed as "not planned".
|
||||
- If it's a feature/enhancement request but well-described, just ask if it's still needed.
|
||||
- If it's a bug, ask if they can reproduce it with the latest build and provide detailed reproduction steps.
|
||||
- If the issue has assignees, append a ping to the assignees to check in.
|
||||
- If it is NOT an Epic AND `is_high_priority` is `false` AND `is_tracked_by_epic` is `false`, append "Feel free to reopen this issue." to the comment.
|
||||
b. Execute `gh issue edit <issue_url> --remove-label "status/need-triage"`. If it is NOT an Epic, also append `--add-label "status/needs-info"`.
|
||||
c. Execute `gh issue comment <issue_url> --body "<your formulated comment>"`
|
||||
d. If it is NOT an Epic AND `is_high_priority` is `false` AND `is_tracked_by_epic` is `false`, execute `gh issue close <issue_url> --reason "not planned"`.
|
||||
- After executing these actions, **STOP EXECUTION**.
|
||||
- If `inactive_over_30_days` is `true` AND it is a bug report (`is_feature_request` is `false`) AND it is NOT an Epic (`is_epic` is `false`):
|
||||
a. Execute `gh issue edit <issue_url> --remove-label "status/need-triage" --add-label "status/needs-info"`.
|
||||
b. Execute `gh issue comment <issue_url> --body "@<reporter_username>, this issue has been inactive for over a month. Could you please try reproducing it with the latest nightly build and let us know if it still occurs? If we don't hear back, we will close this issue on <deadline_date>."`
|
||||
- After executing these actions, **STOP EXECUTION**.
|
||||
- If neither condition is met, proceed to Step 3.
|
||||
|
||||
## Step 3: Vagueness Check
|
||||
Is the issue fundamentally missing context AND no one has asked for more information yet?
|
||||
- **For bugs**: Explicit reproduction steps are **REQUIRED**. Even if the user provides logs, error traces, or screenshots, if they do not provide clear, step-by-step instructions on how to reproduce the bug, it MUST be considered vague.
|
||||
- **For feature requests**: If it is just a vague statement without clear use cases or details, it is considered vague.
|
||||
- If YES (it is vague): Execute `gh issue edit <issue_url> --remove-label "status/need-triage" --add-label "status/needs-info"`. Ask the reporter: `gh issue comment <issue_url> --body "@<reporter_username>, thank you for the report! Could you please provide more specific details (e.g., detailed reproduction steps, expected behavior, logs, and environment details)? Closing this as vague if no response is received in a week."` and **STOP EXECUTION**.
|
||||
- If NO: Proceed to Step 4.
|
||||
|
||||
## Step 4: Reproduction & Code Validity
|
||||
1. Review the issue comments. If a community member has already clearly identified the root cause of the bug or answered the feature request, DO NOT investigate the code. Proceed to Step 5.
|
||||
2. If it is a feature request, check if the feature is already implemented in the project. It could be an existing command, flag, setting, or UI feature. Search the codebase (e.g., `schemas/settings.schema.json`, `packages/cli/src/config/config.ts`, or command definitions) for relevant keywords.
|
||||
- If the feature ALREADY EXISTS: Close the issue: `gh issue close <issue_url> --comment "This feature is actually already implemented! <Provide a brief explanation of how to use the feature, such as the command to run, the setting to change, or the flag to pass>.\n\nI'm going to close this issue since the functionality already exists. Let us know if you run into any other issues!" --reason "completed"` and **STOP EXECUTION**.
|
||||
3. If no root cause is identified and it's not an existing feature, clone the target repository to a temporary directory (`git clone <repo_url> target-repo`).
|
||||
4. Search the `target-repo/` codebase using `grep_search` and `read_file` ONLY. You are explicitly FORBIDDEN from writing new files, running tests, attempting to fix the code, OR attempting to reproduce the bug by executing code or shell commands. Your ONLY goal is to perform STATIC code analysis to determine if the logic for the bug still exists, if the feature is already implemented, or if the reported behavior is actually intentional by design.
|
||||
- If definitively NO LONGER VALID or ALREADY IMPLEMENTED: Close it: `gh issue close <issue_url> --comment "Closing because I have verified this works correctly in the latest codebase. <brief explanation>"` and **STOP EXECUTION**.
|
||||
- If INTENTIONAL BY DESIGN: Close it: `gh issue close <issue_url> --reason "not planned" --comment "Closing this issue as the reported behavior is intentional by design. <brief explanation of the design logic>"` and **STOP EXECUTION**.
|
||||
- If still valid: Proceed to Step 5.
|
||||
|
||||
## Step 5: Duplicates
|
||||
Search for duplicates using `gh issue list --search "<keywords>" --repo <owner/repo> --state all`.
|
||||
- If found: `gh issue close <issue_url> --reason "not planned" --comment "Closing as duplicate of #<duplicate_number>."` and **STOP EXECUTION**.
|
||||
- If no duplicates: Proceed to Step 6.
|
||||
|
||||
## Step 6: Triage Summary
|
||||
Review the issue comments to see if a community member has already identified the root cause.
|
||||
- If a root cause is identified: determine if it is a very simple fix. If it is a simple fix, post guidance for the fix, categorize the issue as **Maintainer-only**, and explain why. If it is not a simple fix, determine whether it should be **Maintainer-only** or **Help-wanted** and explain why.
|
||||
- If no root cause is identified: State whether the issue should be categorized as **Maintainer-only** (epic, core architecture, sensitive fixes, internal tasks, or issues requiring deep investigation) or **Help-wanted** (good for community, general bugs, features, or tasks ready for external help). Your comment should be brief and clearly explain *why* it fits that category.
|
||||
- If you categorized the issue as **Help-wanted**, also run `gh issue edit <issue_url> --remove-label "status/need-triage" --add-label "help wanted"`.
|
||||
- If you categorized the issue as **Maintainer-only**, also run `gh issue edit <issue_url> --remove-label "status/need-triage" --add-label "🔒 maintainer only"`.
|
||||
- Action: `gh issue comment <issue_url> --body "### Triage Summary\n\n<your summary>"`
|
||||
- **STOP EXECUTION**.
|
||||
@@ -0,0 +1,161 @@
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function runCommand(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8' }).trim();
|
||||
} catch (error) {
|
||||
// Return empty or structured error so the agent can see it rather than failing the whole script
|
||||
return JSON.stringify({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeIssue(issueLink, maintainersList) {
|
||||
if (!issueLink) {
|
||||
console.error(JSON.stringify({ error: "Issue link is required." }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parts = issueLink.split('/');
|
||||
const issueNumberStr = parts[parts.length - 1];
|
||||
const issueNumber = parseInt(issueNumberStr, 10);
|
||||
const repoName = parts[parts.length - 3];
|
||||
const repoOwner = parts[parts.length - 4];
|
||||
const repo = `${repoOwner}/${repoName}`;
|
||||
|
||||
const maintainers = maintainersList ? maintainersList.split(',').map(m => m.trim()) : [];
|
||||
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + 14);
|
||||
|
||||
const result = {
|
||||
issue_link: issueLink,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
is_tracked_by_epic: false,
|
||||
is_stale: false,
|
||||
inactive_over_30_days: false,
|
||||
inactive_over_60_days: false,
|
||||
has_assignees: false,
|
||||
is_feature_request: false,
|
||||
is_high_priority: false,
|
||||
is_epic: false,
|
||||
reporter: null,
|
||||
assignees: [],
|
||||
labels: [],
|
||||
cross_references: [],
|
||||
deadline_date: deadline.toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Fetch Issue Data
|
||||
const issueDataRaw = runCommand(`gh issue view ${issueLink} --json title,body,author,comments,labels,assignees,updatedAt`);
|
||||
if (issueDataRaw.startsWith('{"error"')) {
|
||||
console.error(issueDataRaw);
|
||||
process.exit(1);
|
||||
}
|
||||
const issue = JSON.parse(issueDataRaw);
|
||||
|
||||
result.reporter = issue.author.login;
|
||||
result.assignees = issue.assignees.map(a => a.login);
|
||||
result.has_assignees = result.assignees.length > 0;
|
||||
result.labels = issue.labels.map(l => l.name);
|
||||
|
||||
result.is_feature_request = result.labels.some(l => {
|
||||
const lower = l.toLowerCase();
|
||||
return lower.includes('feature') || lower.includes('enhancement');
|
||||
});
|
||||
|
||||
result.is_high_priority = result.labels.some(l => {
|
||||
const lower = l.toLowerCase();
|
||||
return lower.includes('priority/p0') || lower.includes('priority/p1');
|
||||
});
|
||||
|
||||
// 2. Fetch Timeline for cross references
|
||||
const timelineCmd = `gh api repos/${repo}/issues/${issueNumber}/timeline --jq '[.[] | select(.event == "cross-referenced" and .source.issue)] | map({issue: .source.issue.number, state: .source.issue.state, state_reason: .source.issue.state_reason, is_pr: (.source.issue.pull_request != null), is_merged: (.source.issue.pull_request.merged_at != null)})'`;
|
||||
const timelineRaw = runCommand(timelineCmd);
|
||||
if (!timelineRaw.startsWith('{"error"')) {
|
||||
result.cross_references = JSON.parse(timelineRaw);
|
||||
}
|
||||
|
||||
// 3. Check if Epic (has sub issues or title starts with [Epic])
|
||||
const epicCmd = `gh api repos/${repo}/issues/${issueNumber} --jq '{is_epic: (.sub_issues_summary.total > 0)}'`;
|
||||
const epicRaw = runCommand(epicCmd);
|
||||
if (!epicRaw.startsWith('{"error"')) {
|
||||
result.is_epic = JSON.parse(epicRaw).is_epic || issue.title.toLowerCase().startsWith('[epic]');
|
||||
} else {
|
||||
result.is_epic = issue.title.toLowerCase().startsWith('[epic]');
|
||||
}
|
||||
|
||||
// 4. Check for Parent Issue via GraphQL
|
||||
const query = `query($owner: String!, $repo: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issueNumber) { trackedInIssues(first: 1) { totalCount } } } }`;
|
||||
const gqlCmd = `gh api graphql -F owner=${repoOwner} -F repo=${repoName} -F issueNumber=${issueNumber} -f query='${query}' --jq '.data.repository.issue.trackedInIssues.totalCount'`;
|
||||
const parentCountRaw = runCommand(gqlCmd);
|
||||
if (!parentCountRaw.startsWith('{"error"')) {
|
||||
const count = parseInt(parentCountRaw, 10);
|
||||
result.is_tracked_by_epic = !isNaN(count) && count > 0;
|
||||
}
|
||||
|
||||
// Staleness Logic
|
||||
if (result.is_tracked_by_epic) {
|
||||
result.is_stale = true;
|
||||
} else {
|
||||
// Find last maintainer comment
|
||||
const maintainerComments = issue.comments.filter(c => {
|
||||
if (c.author.login === result.reporter) return false;
|
||||
|
||||
const isMaintainer = maintainers.includes(c.author.login) ||
|
||||
['OWNER', 'MEMBER', 'COLLABORATOR'].includes(c.authorAssociation);
|
||||
|
||||
if (maintainers.length > 0) return isMaintainer;
|
||||
// Basic bot exclusion if no maintainers defined
|
||||
return !c.author.login.includes('github-actions') && c.author.login !== 'app/github-actions';
|
||||
});
|
||||
|
||||
if (maintainerComments.length > 0) {
|
||||
const lastMaintainerComment = maintainerComments[maintainerComments.length - 1];
|
||||
|
||||
// Did reporter reply after maintainer?
|
||||
const reporterReplied = issue.comments.some(c => {
|
||||
return c.author.login === result.reporter &&
|
||||
new Date(c.updatedAt) > new Date(lastMaintainerComment.updatedAt);
|
||||
});
|
||||
|
||||
if (!reporterReplied) {
|
||||
const daysAgo = (new Date() - new Date(lastMaintainerComment.updatedAt)) / (1000 * 60 * 60 * 24);
|
||||
if (daysAgo > 7) {
|
||||
result.is_stale = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Age / Inactivity check
|
||||
if (!result.is_stale) {
|
||||
const lastUpdateDaysAgo = (new Date() - new Date(issue.updatedAt)) / (1000 * 60 * 60 * 24);
|
||||
if (lastUpdateDaysAgo > 60) {
|
||||
result.inactive_over_60_days = true;
|
||||
result.inactive_over_30_days = true;
|
||||
} else if (lastUpdateDaysAgo > 30) {
|
||||
result.inactive_over_30_days = true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({ error: error.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const issueLink = args[0];
|
||||
const maintainers = args[1] || ""; // Comma separated list of maintainers
|
||||
|
||||
if (!issueLink) {
|
||||
console.log("Usage: node analyze_issue.cjs <issue_link> [maintainers_csv]");
|
||||
console.log("Example: node analyze_issue.cjs https://github.com/owner/repo/issues/123 'user1,user2'");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
analyzeIssue(issueLink, maintainers);
|
||||
@@ -0,0 +1,34 @@
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function findIssues(repo, searchString = "label:area/core,area/extensions,area/site,area/non-interactive sort:updated-asc", limit = 10) {
|
||||
if (!repo) {
|
||||
console.error(JSON.stringify({ error: "Repository is required (e.g., owner/repo)" }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const cmd = `gh issue list --repo ${repo} --state open --search "${searchString}" --json url --limit ${limit}`;
|
||||
const output = execSync(cmd, { encoding: 'utf-8' });
|
||||
const issues = JSON.parse(output);
|
||||
const urls = issues.map(issue => issue.url);
|
||||
|
||||
console.log(JSON.stringify({ issue_urls: urls }, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({ error: error.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const repo = args[0];
|
||||
const searchString = args[1];
|
||||
const limitStr = args[2];
|
||||
const limit = limitStr ? parseInt(limitStr, 10) : 10;
|
||||
|
||||
if (!repo) {
|
||||
console.log("Usage: node find_issues.cjs <owner/repo> [search_string] [limit]");
|
||||
console.log("Example: node find_issues.cjs google-gemini/gemini-cli 'label:area/core sort:updated-asc' 10");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
findIssues(repo, searchString, limit);
|
||||
Reference in New Issue
Block a user