mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-20 04:19:57 +00:00
281 lines
11 KiB
YAML
281 lines
11 KiB
YAML
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})`
|
|
});
|