chore: remove foreign files (retro_games, social-map-care leftovers) #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| }); | |
| } |