fix(workflows): improve issue triage (#7449)

Co-authored-by: Srinath Padmanabhan <srithreepo@google.com>
This commit is contained in:
Srinath Padmanabhan
2025-08-29 19:47:06 -07:00
committed by GitHub
parent 001009d350
commit ab1b74802d
2 changed files with 129 additions and 47 deletions
@@ -16,7 +16,7 @@ on:
type: 'number' type: 'number'
concurrency: concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number }}' group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number }}'
cancel-in-progress: true cancel-in-progress: true
defaults: defaults:
@@ -29,22 +29,51 @@ permissions:
issues: 'write' issues: 'write'
statuses: 'write' statuses: 'write'
packages: 'read' packages: 'read'
actions: 'write' # Required for cancelling a workflow run
jobs: jobs:
triage-issue: triage-issue:
if: |- if: |-
github.repository == 'google-gemini/gemini-cli' && github.repository == 'google-gemini/gemini-cli' &&
(github.event_name == 'issues' || (
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && (
contains(github.event.comment.body, '@gemini-cli /triage') && (github.event_name == 'issues' || github.event_name == 'issue_comment') &&
(github.event.comment.author_association == 'OWNER' || contains(github.event.issue.labels.*.name, 'status/need-triage') &&
github.event.comment.author_association == 'MEMBER' || (github.event_name != 'issue_comment' || (
github.event.comment.author_association == 'COLLABORATOR'))) contains(github.event.comment.body, '@gemini-cli /triage') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
))
)
)
timeout-minutes: 5 timeout-minutes: 5
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- name: 'Get issue data for manual trigger'
id: 'get_issue_data'
if: |-
github.event_name == 'workflow_dispatch'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.inputs.issue_number }},
});
core.setOutput('title', issue.title);
core.setOutput('body', issue.body);
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
return issue;
- name: 'Check for triage label on manual trigger'
if: |-
github.event_name == 'workflow_dispatch' && !contains(steps.get_issue_data.outputs.labels, 'status/need-triage')
run: |
echo "Issue #${{ github.event.inputs.issue_number }} does not have the 'status/need-triage' label. Stopping workflow."
exit 1
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -60,7 +89,7 @@ jobs:
id: 'get_labels' id: 'get_labels'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
const { data: labels } = await github.rest.issues.listLabelsForRepo({ const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner, owner: context.repo.owner,
@@ -76,9 +105,12 @@ jobs:
id: 'gemini_issue_analysis' id: 'gemini_issue_analysis'
env: env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
ISSUE_TITLE: '${{ github.event.issue.title }}' ISSUE_TITLE: >-
ISSUE_BODY: '${{ github.event.issue.body }}' ${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
ISSUE_NUMBER: '${{ github.event.issue.number }}' ISSUE_BODY: >-
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
ISSUE_NUMBER: >-
${{ github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number || github.event.issue.number }}
REPOSITORY: '${{ github.repository }}' REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
with: with:
@@ -104,18 +136,20 @@ jobs:
## Role ## Role
You are an issue triage assistant. Analyze the current GitHub issue You are an issue triage assistant. Analyze the current GitHub issue
and identify the most appropriate existing labels. Use the available and identify the most appropriate existing labels by only using the provided data. Use the available
tools to gather information; do not ask for information to be tools to gather information; do not ask for information to be
provided. Do not remove labels titled help wanted or good first issue. provided. Do not remove labels titled help wanted or good first issue.
## Steps ## Steps
1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}". 1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
2. Review the issue title and body provided in the environment variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}". 2. Review the issue title and body provided in the environment variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
3. Ignore any existing priorities or tags on the issue. Just report your findings. 3. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. For area/* and kind/* limit yourself to only the single most applicable label in each case.
4. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. For area/* and kind/* limit yourself to only the single most applicable label in each case. 4. If the issue already has area/ label, dont try to change it. Similarly, if the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
If an issue has area/core and kind/bug you will only add a priority/ label.
Instead if an issue has no labels, you will could add one lable of each kind.
5. 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 for anything more than 6 versions older than the most recent should add the status/need-retesting label. 5. 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 for anything more than 6 versions older than the most recent should add the status/need-retesting label.
6. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label. 6. 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.
7. Output the appropriate labels for this issue in JSON format with explanation, for example: 7. Output the appropriate labels for this issue in JSON format with explanation, for example:
``` ```
{"labels_to_set": ["kind/bug", "priority/p0"], "explanation": "This is a critical bug report affecting main functionality"} {"labels_to_set": ["kind/bug", "priority/p0"], "explanation": "This is a critical bug report affecting main functionality"}
@@ -125,6 +159,7 @@ jobs:
{"labels_to_set": [], "explanation": "Unable to classify this issue with available labels"} {"labels_to_set": [], "explanation": "Unable to classify this issue with available labels"}
``` ```
9. Use Area definitions mentioned below to help you narrow down issues. 9. Use Area definitions mentioned below to help you narrow down issues.
10. If you are uncertain and have not been able to apply one each of kind/, area/ and priority/ , apply the status/manual-triage label.
## Guidelines ## Guidelines
@@ -221,19 +256,23 @@ jobs:
${{ steps.gemini_issue_analysis.outputs.summary != '' }} ${{ steps.gemini_issue_analysis.outputs.summary != '' }}
env: env:
REPOSITORY: '${{ github.repository }}' REPOSITORY: '${{ github.repository }}'
ISSUE_NUMBER: '${{ github.event.issue.number }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}' LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
// Strip code block markers if present // Strip code block markers if present
const rawLabels = process.env.LABELS_OUTPUT; const rawLabels = process.env.LABELS_OUTPUT;
core.info(`Raw labels JSON: ${rawLabels}`); core.info(`Raw labels JSON: ${rawLabels}`);
let parsedLabels; let parsedLabels;
try { try {
const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim(); const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
parsedLabels = JSON.parse(trimmedLabels); if (!jsonMatch || !jsonMatch[1]) {
throw new Error("Could not find a ```json ... ``` block in the output.");
}
const jsonString = jsonMatch[1].trim();
parsedLabels = JSON.parse(jsonString);
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`); core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
} catch (err) { } catch (err) {
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`); core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
@@ -241,6 +280,7 @@ jobs:
} }
const issueNumber = parseInt(process.env.ISSUE_NUMBER); const issueNumber = parseInt(process.env.ISSUE_NUMBER);
const explanation = parsedLabels.explanation || '';
// Set labels based on triage result // Set labels based on triage result
if (parsedLabels.labels_to_set && parsedLabels.labels_to_set.length > 0) { if (parsedLabels.labels_to_set && parsedLabels.labels_to_set.length > 0) {
@@ -250,23 +290,32 @@ jobs:
issue_number: issueNumber, issue_number: issueNumber,
labels: parsedLabels.labels_to_set labels: parsedLabels.labels_to_set
}); });
const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : ''; const explanationInfo = explanation ? ` - ${explanation}` : '';
core.info(`Successfully set labels for #${issueNumber}: ${parsedLabels.labels_to_set.join(', ')}${explanation}`); core.info(`Successfully set labels for #${issueNumber}: ${parsedLabels.labels_to_set.join(', ')}${explanationInfo}`);
} else { } else {
// If no labels to set, leave the issue as is // If no labels to set, leave the issue as is
const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : ''; const explanationInfo = explanation ? ` - ${explanation}` : '';
core.info(`No labels to set for #${issueNumber}, leaving as is${explanation}`); core.info(`No labels to set for #${issueNumber}, leaving as is${explanationInfo}`);
}
if (explanation) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: explanation,
});
} }
- name: 'Post Issue Analysis Failure Comment' - name: 'Post Issue Analysis Failure Comment'
if: |- if: |-
${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }} ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
env: env:
ISSUE_NUMBER: '${{ github.event.issue.number }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
github.rest.issues.createComment({ github.rest.issues.createComment({
owner: context.repo.owner, owner: context.repo.owner,
@@ -49,7 +49,7 @@ jobs:
echo '🏷️ Finding issues that need triage...' echo '🏷️ Finding issues that need triage...'
NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue label:"status/needs-triage"' --json number,title,body)" --search "is:open is:issue label:\"status/need-triage\"" --limit 1000 --json number,title,body)"
echo '🔄 Merging and deduplicating issues...' echo '🔄 Merging and deduplicating issues...'
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')" ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
@@ -64,7 +64,7 @@ jobs:
id: 'get_labels' id: 'get_labels'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
const { data: labels } = await github.rest.issues.listLabelsForRepo({ const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner, owner: context.repo.owner,
@@ -113,11 +113,13 @@ jobs:
## Steps ## Steps
1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}". 1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues) 2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
3. Review the issue title, body and any comments provided in the environment variables. 3. Review the issue title, body and any comments provided in the environment variables.
4. Ignore any existing priorities or tags on the issue. 4. Identify the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*.
5. Identify the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. 5. If the issue already has area/ label, dont try to change it. Similarly, if the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
If an issue has area/core and kind/bug you will only add a priority/ label.
Instead if an issue has no labels, you will could add one lable of each kind.
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc. 6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
7. For area/* and kind/* limit yourself to only the single most applicable label in each case. 7. For area/* and kind/* limit yourself to only the single most applicable label in each case.
8. Give me a single short explanation about why you are selecting each label in the process. 8. Give me a single short explanation about why you are selecting each label in the process.
@@ -128,7 +130,7 @@ jobs:
{ {
"issue_number": 123, "issue_number": 123,
"labels_to_add": ["kind/bug", "priority/p2"], "labels_to_add": ["kind/bug", "priority/p2"],
"labels_to_remove": ["status/needs-triage"], "labels_to_remove": ["status/need-triage"],
"explanation": "This issue is a bug that needs to be addressed with medium priority." "explanation": "This issue is a bug that needs to be addressed with medium priority."
}, },
{ {
@@ -142,8 +144,9 @@ jobs:
If an issue cannot be classified, do not include it in the output array. If an issue cannot be classified, do not include it in the output array.
10. 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 10. 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 - Anything more than 6 versions older than the most recent should add the status/need-retesting label
11. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label 11. 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.
- After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output. - After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output.
12. If you are uncertain and have not been able to apply one each of kind/, area/ and priority/ , apply the status/manual-triage label.
## Guidelines ## Guidelines
@@ -252,15 +255,18 @@ jobs:
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}' LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
// Strip code block markers if present
const rawLabels = process.env.LABELS_OUTPUT; const rawLabels = process.env.LABELS_OUTPUT;
core.info(`Raw labels JSON: ${rawLabels}`); core.info(`Raw labels JSON: ${rawLabels}`);
let parsedLabels; let parsedLabels;
try { try {
const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim(); const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
parsedLabels = JSON.parse(trimmedLabels); if (!jsonMatch || !jsonMatch[1]) {
throw new Error("Could not find a ```json ... ``` block in the output.");
}
const jsonString = jsonMatch[1].trim();
parsedLabels = JSON.parse(jsonString);
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`); core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
} catch (err) { } catch (err) {
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`); core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
@@ -274,18 +280,45 @@ jobs:
continue; continue;
} }
// Set labels based on triage result if (entry.labels_to_add && entry.labels_to_add.length > 0) {
if (entry.labels_to_set && entry.labels_to_set.length > 0) { await github.rest.issues.addLabels({
await github.rest.issues.setLabels({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
issue_number: issueNumber, issue_number: issueNumber,
labels: entry.labels_to_set labels: entry.labels_to_add
}); });
const explanation = entry.explanation ? ` - ${entry.explanation}` : ''; const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
core.info(`Successfully set labels for #${issueNumber}: ${entry.labels_to_set.join(', ')}${explanation}`); core.info(`Successfully added labels for #${issueNumber}: ${entry.labels_to_add.join(', ')}${explanation}`);
} else { }
// If no labels to set, leave the issue as is
core.info(`No labels to set for #${issueNumber}, leaving as is`); if (entry.labels_to_remove && entry.labels_to_remove.length > 0) {
for (const label of entry.labels_to_remove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
core.info(`Successfully removed labels for #${issueNumber}: ${entry.labels_to_remove.join(', ')}`);
}
if (entry.explanation) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: entry.explanation,
});
}
if ((!entry.labels_to_add || entry.labels_to_add.length === 0) && (!entry.labels_to_remove || entry.labels_to_remove.length === 0)) {
core.info(`No labels to add or remove for #${issueNumber}, leaving as is`);
} }
} }