Skip to content

chore: remove foreign files (retro_games, social-map-care leftovers) #1

chore: remove foreign files (retro_games, social-map-care leftovers)

chore: remove foreign files (retro_games, social-map-care leftovers) #1

name: '📋 Gemini Scheduled Issue Triage'
on:
schedule:
- cron: '0 * * * *' # Runs every hour
pull_request:
branches:
- 'main'
- 'release/**/*'
paths:
- '.github/workflows/gemini-scheduled-triage.yml'
push:
branches:
- 'main'
- 'release/**/*'
paths:
- '.github/workflows/gemini-scheduled-triage.yml'
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
triage:
runs-on: 'ubuntu-latest'
timeout-minutes: 20
permissions:
contents: 'read'
id-token: 'write'
issues: 'read'
pull-requests: 'read'
outputs:
available_labels: '${{ steps.get_labels.outputs.available_labels }}'
triaged_issues: '${{ steps.capture_output.outputs.gemini_json }}'
steps:
- name: 'Get repository labels'
id: 'get_labels'
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # v8.0.0
with:
# NOTE: we intentionally do not use the minted token. The default
# GITHUB_TOKEN provided by the action has enough permissions to read
# the labels.
script: |
const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});
if (!labels || labels.length === 0) {
core.setFailed('There are no issue labels in this repository.')
}
const labelNames = labels.map(label => label.name).sort();
core.setOutput('available_labels', labelNames.join(','));
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames;
- name: 'Find untriaged issues'
id: 'find_issues'
env:
GITHUB_REPOSITORY: '${{ github.repository }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |-
export HAS_ISSUES=$(gh api repos/${GITHUB_REPOSITORY} --jq '.has_issues')
if [[ "$HAS_ISSUES" == "false" ]]; then
echo '📝 GitHub repo does not have Issues enabled. Skipping issue triage.'
echo "issues_to_triage=[]" >> "${GITHUB_OUTPUT}"
exit
fi
echo '🔍 Finding unlabeled issues and issues marked for triage...'
UNLABELED_ISSUES="$(gh issue list \
--state 'open' \
--search 'no:label' \
--json number,title,body \
--limit '20' \
--repo "${GITHUB_REPOSITORY}" || echo "[]"
)"
NEEDS_TRIAGE_ISSUES="$(gh issue list \
--state 'open' \
--search 'label:"status/needs-triage"' \
--json number,title,body \
--limit '20' \
--repo "${GITHUB_REPOSITORY}" || echo "[]"
)"
ISSUES="$(echo "${UNLABELED_ISSUES}" "${NEEDS_TRIAGE_ISSUES}" | jq -s 'add | unique_by(.number) | . // []')"
echo "${ISSUES}" | jq . || { echo "ERROR: Output is not valid JSON"; exit 1; }
echo '📝 Setting output for GitHub Actions...'
echo "issues_to_triage<<EOF" >> "${GITHUB_OUTPUT}"
echo "${ISSUES}" >> "${GITHUB_OUTPUT}"
echo "EOF" >> "${GITHUB_OUTPUT}"
ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "✅ Found ${ISSUE_COUNT} issue(s) to triage! 🎯"
- name: 'Run Gemini Issue Analysis'
id: 'gemini_issue_analysis'
if: |-
${{ steps.find_issues.outputs.issues_to_triage != '[]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
with:
gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
google_api_key: '${{ secrets.GOOGLE_API_KEY }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
gemini_debug: '${{ fromJSON(vars.DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}'
gemini_model: '${{ vars.GEMINI_MODEL }}'
settings: |-
{
"maxSessionTurns": 50,
"telemetry": {
"enabled": ${{ vars.GOOGLE_CLOUD_PROJECT != '' }},
"target": "gcp"
},
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(jq)",
"run_shell_command(printenv)"
]
}
prompt: |-
You are an expert Issue Triage Engineer. Your task is to analyze a list of GitHub issues and assign the most relevant labels.
**Instructions:**
1. **Retrieve Data**: Immediately run `printenv ISSUES_TO_TRIAGE` and `printenv AVAILABLE_LABELS` to get the input data.
2. **Batch Analysis**: Analyze all issues in the list internally. Do not output analysis for individual issues.
3. **Generate Output**: Produce a single, minified JSON array containing the triage results.
4. **Format**: The output must be a valid JSON array of objects with keys: `issue_number`, `labels_to_set`, `explanation`.
5. **Constraints**:
- Perform the analysis yourself. Do NOT write a script (Python, Bash, etc.) to do it.
- Only use labels from `AVAILABLE_LABELS`.
- If an issue does not clearly match a specific label, apply "status/needs-triage".
- Do not use `jq` or other tools to parse the input; read the variables directly.
- Your final output to STDOUT must only be the JSON array.
- IMPORTANT: Do not format the output as markdown. Do not use backticks (```). Just output the raw JSON string.
- Do not include any other text or explanations.
**Example Output:**
[
{
"issue_number": 123,
"labels_to_set": ["kind/bug", "priority/p2"],
"explanation": "The issue describes a critical error in the login functionality."
}
]
- name: 'Capture Gemini Output'
id: 'capture_output'
if: |-
${{ steps.find_issues.outputs.issues_to_triage != '[]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
run: |-
if [ -f gemini-artifacts/stdout.log ]; then
echo "::group::Gemini Output Content"
cat gemini-artifacts/stdout.log
echo "::endgroup::"
echo "gemini_json<<EOF" >> "$GITHUB_OUTPUT"
cat gemini-artifacts/stdout.log >> "$GITHUB_OUTPUT"
echo "" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
else
echo "::warning::Gemini stdout artifact not found."
echo "gemini_json=[]" >> "$GITHUB_OUTPUT"
fi
label:
runs-on: 'ubuntu-latest'
needs:
- 'triage'
if: |-
needs.triage.outputs.available_labels != '' &&
needs.triage.outputs.available_labels != '[]' &&
needs.triage.outputs.triaged_issues != '' &&
needs.triage.outputs.triaged_issues != '[]'
permissions:
contents: 'read'
issues: 'write'
pull-requests: 'write'
steps:
- name: 'Mint identity token'
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
permission-contents: 'read'
permission-issues: 'write'
permission-pull-requests: 'write'
- name: 'Apply labels'
env:
AVAILABLE_LABELS: '${{ needs.triage.outputs.available_labels }}'
TRIAGED_ISSUES: '${{ needs.triage.outputs.triaged_issues }}'
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # v8.0.0
with:
# Use the provided token so that the "gemini-cli" is the actor in the
# log for what changed the labels.
github-token: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
script: |-
// Parse the available labels
const availableLabels = (process.env.AVAILABLE_LABELS || '').split(',')
.map((label) => label.trim())
.sort()
// Robustly parse the Gemini Output
let rawInput = process.env.TRIAGED_ISSUES || '';
core.debug(`Raw Gemini Output: ${rawInput}`);
// 1. Clean up potential log pollution (e.g. "Creating GCP exporters...")
// We assume the actual output is a JSON object starting with '{' (Session Object)
// or a JSON array starting with '[' (Direct Output).
const objectStart = rawInput.indexOf('{');
const arrayStart = rawInput.indexOf('[');
let jsonStartIndex = -1;
if (objectStart !== -1 && (arrayStart === -1 || objectStart < arrayStart)) {
jsonStartIndex = objectStart;
} else if (arrayStart !== -1) {
jsonStartIndex = arrayStart;
}
if (jsonStartIndex === -1) {
core.setFailed('No JSON start character ({ or [) found in Gemini output.');
return;
}
// Clean input starting from the first JSON character
rawInput = rawInput.substring(jsonStartIndex);
let triagedIssues = [];
try {
// 2. Parse the outer layer
const parsedOutput = JSON.parse(rawInput);
// 3. Handle Session Object vs Direct Array
if (Array.isArray(parsedOutput)) {
// It was a direct array (ideal case)
triagedIssues = parsedOutput;
} else if (parsedOutput.response) {
// It was a Session Object (run-gemini-cli default)
core.info('Detected Session Object, extracting response field...');
let responseText = parsedOutput.response;
// 4. Clean up inner Markdown/Text
const innerArrayStart = responseText.indexOf('[');
const innerArrayEnd = responseText.lastIndexOf(']');
if (innerArrayStart !== -1 && innerArrayEnd !== -1) {
responseText = responseText.substring(innerArrayStart, innerArrayEnd + 1);
triagedIssues = JSON.parse(responseText);
} else {
// If we can't find an array, maybe the model failed to output one.
// We'll log the response for debugging and treat it as empty.
core.warning(`Could not find JSON array in model response: ${parsedOutput.response}`);
triagedIssues = [];
}
} else {
core.warning('Parsed object is neither an array nor a Session Object with a response field.');
core.debug(JSON.stringify(parsedOutput));
}
} catch (e) {
core.setFailed(`Failed to parse Gemini output: ${e.message}`);
return;
}
// Sort results
triagedIssues.sort((a, b) => a.issue_number - b.issue_number);
core.debug(`Final Triaged issues: ${JSON.stringify(triagedIssues)}`);
// Iterate over each label
for (const issue of triagedIssues) {
if (!issue) {
core.debug(`Skipping empty issue: ${JSON.stringify(issue)}`);
continue;
}
const issueNumber = issue.issue_number;
if (!issueNumber) {
core.debug(`Skipping issue with no data: ${JSON.stringify(issue)}`);
continue;
}
// Extract and reject invalid labels - we do this just in case
// someone was able to prompt inject malicious labels.
let labelsToSet = (issue.labels_to_set || [])
.map((label) => label.trim())
.filter((label) => availableLabels.includes(label))
.sort()
core.debug(`Identified labels to set: ${JSON.stringify(labelsToSet)}`);
if (labelsToSet.length === 0) {
core.info(`Skipping issue #${issueNumber} - no labels to set.`)
continue;
}
core.debug(`Setting labels on issue #${issueNumber} to ${labelsToSet.join(', ')} (${issue.explanation || 'no explanation'})`)
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToSet,
});
}