This repository was archived by the owner on Mar 30, 2026. It is now read-only.
Feature Request: Support Antigravity AI Credit Overages for seamless quota fallback #1459
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: '🏷️ Issue Triage' | |
| on: | |
| issues: | |
| types: | |
| - 'opened' | |
| - 'reopened' | |
| issue_comment: | |
| types: | |
| - 'created' | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: 'Issue number to triage' | |
| required: true | |
| type: 'number' | |
| concurrency: | |
| group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number }}' | |
| cancel-in-progress: true | |
| permissions: | |
| contents: 'read' | |
| issues: 'write' | |
| jobs: | |
| triage-issue: | |
| if: | | |
| github.event_name == 'workflow_dispatch' || | |
| github.event_name == 'issues' || | |
| ( | |
| github.event_name == 'issue_comment' && | |
| contains(github.event.comment.body, '/triage') && | |
| ( | |
| github.event.comment.author_association == 'OWNER' || | |
| github.event.comment.author_association == 'MEMBER' || | |
| github.event.comment.author_association == 'COLLABORATOR' | |
| ) | |
| ) | |
| timeout-minutes: 30 | |
| runs-on: self-hosted | |
| steps: | |
| - name: 'Check if already triaged' | |
| id: 'check_labels' | |
| if: github.event_name != 'workflow_dispatch' | |
| uses: 'actions/github-script@v7' | |
| with: | |
| script: | | |
| const labels = context.payload.issue?.labels?.map(l => l.name) || []; | |
| const triageLabels = ['bug', 'enhancement', 'documentation', 'question', 'invalid']; | |
| const areaLabels = labels.filter(l => l.startsWith('area/')); | |
| const hasTriageLabel = labels.some(l => triageLabels.includes(l)) || areaLabels.length > 0; | |
| const isRetriage = context.payload.comment?.body?.includes('/triage'); | |
| if (hasTriageLabel && !labels.includes('needs-triage') && !isRetriage) { | |
| core.info(`Issue already triaged: ${labels.join(', ')}. Skipping.`); | |
| core.setOutput('skip', 'true'); | |
| } else { | |
| core.setOutput('skip', 'false'); | |
| } | |
| - name: 'Get issue data' | |
| id: 'get_issue' | |
| if: steps.check_labels.outputs.skip != 'true' | |
| uses: 'actions/github-script@v7' | |
| with: | |
| script: | | |
| const issueNumber = context.payload.inputs?.issue_number || context.issue?.number; | |
| core.setOutput('number', issueNumber); | |
| - name: 'Run Opencode Triage' | |
| id: 'opencode_triage' | |
| if: steps.check_labels.outputs.skip != 'true' | |
| run: | | |
| echo "Running Opencode Triage on issue ${{ steps.get_issue.outputs.number }}..." | |
| # Move to the repository | |
| cd /home/admin/coding/opencode-antigravity-auth | |
| # Run CLI with the agent config | |
| OUTPUT=$(opencode run --agent triage-bot "Triage issue ${{ steps.get_issue.outputs.number }}") | |
| echo "Opencode Output: $OUTPUT" | |
| echo "result<<EOF" >> $GITHUB_OUTPUT | |
| echo "$OUTPUT" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: 'Apply Labels and Respond' | |
| if: steps.check_labels.outputs.skip != 'true' | |
| uses: 'actions/github-script@v7' | |
| env: | |
| ISSUE_NUMBER: '${{ steps.get_issue.outputs.number }}' | |
| OPENCODE_OUTPUT: '${{ steps.opencode_triage.outputs.result }}' | |
| with: | |
| script: | | |
| const rawOutput = process.env.OPENCODE_OUTPUT; | |
| if (!rawOutput) { | |
| core.warning('No triage output available'); | |
| return; | |
| } | |
| core.info(`Triage output: ${rawOutput}`); | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(rawOutput.trim()); | |
| } catch (e) { | |
| // Try to extract JSON from code blocks first | |
| const codeBlockMatch = rawOutput.match(/```(?:json)?\s*([\s\S]*?)\s*```/); | |
| // Find the LAST JSON object containing triage fields (avoid matching code snippets) | |
| // Match JSON objects that contain "type_label" to ensure we get the triage output | |
| const triageJsonMatch = rawOutput.match(/\{"type_label"[\s\S]*?\}(?=\s*$|\s*```|\s*\n\n)/); | |
| // Fallback: find all potential JSON objects and try parsing from the end | |
| let jsonStr = codeBlockMatch?.[1] || triageJsonMatch?.[0]; | |
| if (!jsonStr) { | |
| // Last resort: find all { } pairs and try parsing from the last one | |
| const allMatches = [...rawOutput.matchAll(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g)]; | |
| for (let i = allMatches.length - 1; i >= 0; i--) { | |
| try { | |
| const candidate = JSON.parse(allMatches[i][0]); | |
| if (candidate.type_label && candidate.area_label) { | |
| jsonStr = allMatches[i][0]; | |
| break; | |
| } | |
| } catch {} | |
| } | |
| } | |
| if (jsonStr) { | |
| try { | |
| parsed = JSON.parse(jsonStr.trim()); | |
| } catch (e2) { | |
| core.setFailed(`Failed to parse output: ${rawOutput}`); | |
| return; | |
| } | |
| } else { | |
| core.setFailed(`Failed to parse output: ${rawOutput}`); | |
| return; | |
| } | |
| } | |
| const typeLabel = parsed.type_label; | |
| const areaLabel = parsed.area_label; | |
| const duplicateOf = parsed.duplicate_of; | |
| const suggestedResponse = parsed.suggested_response; | |
| // Validate required fields | |
| if (!typeLabel || !areaLabel || suggestedResponse === undefined) { | |
| core.setFailed(`Invalid JSON structure: missing required fields. Parsed: ${JSON.stringify(parsed)}`); | |
| return; | |
| } | |
| const validTypeLabels = ['bug', 'enhancement', 'documentation', 'question', 'invalid']; | |
| const validAreaLabels = ['area/auth', 'area/models', 'area/config', 'area/compat']; | |
| if (!validTypeLabels.includes(typeLabel)) { | |
| core.warning(`Invalid type_label: ${typeLabel}`); | |
| return; | |
| } | |
| if (!validAreaLabels.includes(areaLabel)) { | |
| core.warning(`Invalid area_label: ${areaLabel}`); | |
| return; | |
| } | |
| let labelsToAdd = []; | |
| let labelsToRemove = ['needs-triage']; | |
| // Remove existing triage labels to prevent conflicts | |
| validTypeLabels.forEach(label => labelsToRemove.push(label)); | |
| validAreaLabels.forEach(label => labelsToRemove.push(label)); | |
| // Only add one type label and one area label | |
| if (typeLabel && validTypeLabels.includes(typeLabel)) { | |
| labelsToAdd.push(typeLabel); | |
| } | |
| if (areaLabel && validAreaLabels.includes(areaLabel)) { | |
| labelsToAdd.push(areaLabel); | |
| } | |
| if (duplicateOf) { | |
| labelsToAdd.push('duplicate'); | |
| } | |
| // Validate single label selection and deduplicate | |
| const typeLabels = labelsToAdd.filter(label => validTypeLabels.includes(label)); | |
| const areaLabels = labelsToAdd.filter(label => validAreaLabels.includes(label)); | |
| const otherLabels = labelsToAdd.filter(label => !validTypeLabels.includes(label) && !validAreaLabels.includes(label)); | |
| if (typeLabels.length > 1) { | |
| core.warning(`Multiple type labels detected: ${typeLabels.join(', ')}. Using only: ${typeLabel}`); | |
| } | |
| if (areaLabels.length > 1) { | |
| core.warning(`Multiple area labels detected: ${areaLabels.join(', ')}. Using only: ${areaLabel}`); | |
| } | |
| // Rebuild labelsToAdd with single type and area labels | |
| labelsToAdd = []; | |
| if (typeLabel && validTypeLabels.includes(typeLabel)) { | |
| labelsToAdd.push(typeLabel); | |
| } | |
| if (areaLabel && validAreaLabels.includes(areaLabel)) { | |
| labelsToAdd.push(areaLabel); | |
| } | |
| labelsToAdd.push(...otherLabels); | |
| // Avoid removing labels that are being added | |
| labelsToRemove = labelsToRemove.filter(label => !labelsToAdd.includes(label)); | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| labels: labelsToAdd | |
| }); | |
| core.info(`Added labels: ${labelsToAdd.join(', ')}`); | |
| for (const label of labelsToRemove) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| name: label | |
| }); | |
| core.info(`Removed label: ${label}`); | |
| } catch (e) { | |
| core.info(`Label ${label} not present, skipping removal`); | |
| } | |
| } | |
| if (suggestedResponse && suggestedResponse.trim()) { | |
| let body = `👋 Thanks for opening this issue!\n\n${suggestedResponse}`; | |
| if (duplicateOf) { | |
| body += `\n\n🔗 This appears to be related to #${duplicateOf}. Please check that issue for updates.`; | |
| } | |
| body += `\n\n---\n*This is an automated response. A maintainer will review your issue soon.*`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: body | |
| }); | |
| core.info('Posted response comment'); | |
| } | |
| if (duplicateOf) { | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| state: 'closed', | |
| state_reason: 'not_planned' | |
| }); | |
| core.info(`Closed as duplicate of #${duplicateOf}`); | |
| } | |
| - name: 'Comment on failure' | |
| if: failure() | |
| uses: 'actions/github-script@v7' | |
| env: | |
| ISSUE_NUMBER: '${{ steps.get_issue.outputs.number || github.event.issue.number }}' | |
| RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| with: | |
| script: | | |
| if (!process.env.ISSUE_NUMBER) return; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: parseInt(process.env.ISSUE_NUMBER), | |
| body: `⚠️ Automated triage failed. [View logs](${process.env.RUN_URL})` | |
| }); |