70bf21e064
Handle Changesets / Handle Changesets (push) Waiting to run
Semgrep OSS scan / semgrep-oss (push) Waiting to run
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
236 lines
8.4 KiB
YAML
236 lines
8.4 KiB
YAML
name: Triage Issue
|
|
|
|
on:
|
|
issues:
|
|
types: [opened]
|
|
issue_comment:
|
|
types: [created]
|
|
workflow_dispatch:
|
|
inputs:
|
|
issue-number:
|
|
description: "Issue number to triage"
|
|
required: true
|
|
type: number
|
|
|
|
permissions:
|
|
contents: read
|
|
# Writes (comment + labels) are performed with GH_ACCESS_TOKEN (the
|
|
# workers-devprod bot) so they are attributed to that identity, not
|
|
# github-actions[bot]. Only read access is needed from the default token.
|
|
issues: read
|
|
|
|
# Cancel in-progress runs for the same issue
|
|
concurrency:
|
|
group: triage-${{ github.event.issue.number || inputs.issue-number }}
|
|
cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }}
|
|
|
|
jobs:
|
|
triage:
|
|
runs-on: ubuntu-latest
|
|
# Always run for manual dispatch. For issue events, skip PRs and
|
|
# bot-created issues. For comment events, skip PRs, bot comments, and the
|
|
# triage bot's own sticky comment (identified by the marocchino marker) to
|
|
# avoid infinite re-triage loops.
|
|
if: >-
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'issues' &&
|
|
!github.event.issue.pull_request &&
|
|
github.event.issue.user.type != 'Bot') ||
|
|
(github.event_name == 'issue_comment' &&
|
|
!github.event.issue.pull_request &&
|
|
github.event.comment.user.type != 'Bot' &&
|
|
!contains(github.event.comment.body, '<!-- Sticky Pull Request Comment'))
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Resolve issue number
|
|
id: issue
|
|
env:
|
|
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
INPUT_ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
run: |
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
|
echo "number=${INPUT_ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "number=${EVENT_ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Checkout (sparse — just the skill and opencode config)
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
sparse-checkout: |
|
|
.github/skills
|
|
.github/opencode.json
|
|
|
|
- name: Install OpenCode
|
|
run: |
|
|
npm install -g opencode-ai
|
|
cp .github/opencode.json ./opencode.json
|
|
|
|
- name: Fetch issue data
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ISSUE: ${{ steps.issue.outputs.number }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
mkdir -p "data/${ISSUE}"
|
|
|
|
gh issue view "$ISSUE" \
|
|
--repo "$REPO" \
|
|
--json number,title,body,comments,createdAt,updatedAt,labels,state,author \
|
|
> "data/${ISSUE}/context.json"
|
|
|
|
- name: Analyze issue with OpenCode
|
|
env:
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }}
|
|
CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }}
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }}
|
|
ISSUE: ${{ steps.issue.outputs.number }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
opencode run \
|
|
--print-logs \
|
|
"Analyze GitHub issue ${REPO}#${ISSUE} using the skill at .github/skills/issue-review.md.
|
|
|
|
The issue data has been pre-fetched to data/${ISSUE}/context.json.
|
|
Read it with the read tool and follow the triage process.
|
|
|
|
Create:
|
|
- data/${ISSUE}/report.md (full markdown report)
|
|
- data/${ISSUE}/summary.json (structured JSON summary)
|
|
"
|
|
|
|
- name: Upload report to dashboard
|
|
env:
|
|
CF_ACCESS_CLIENT_ID: ${{ secrets.CF1_ACCESS_CLIENT_ID }}
|
|
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF1_ACCESS_CLIENT_SECRET }}
|
|
DASHBOARD_URL: ${{ secrets.REPORTS_DASHBOARD_URL }}
|
|
ISSUE: ${{ steps.issue.outputs.number }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
# Check files were created
|
|
if [ ! -f "data/${ISSUE}/report.md" ]; then
|
|
echo "::error::report.md was not generated"
|
|
exit 1
|
|
fi
|
|
if [ ! -f "data/${ISSUE}/summary.json" ]; then
|
|
echo "::error::summary.json was not generated"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate the summary is parseable JSON before doing anything with it.
|
|
if ! jq empty "data/${ISSUE}/summary.json" 2>/dev/null; then
|
|
echo "::error::summary.json is not valid JSON"
|
|
exit 1
|
|
fi
|
|
|
|
# Build the dashboard payload from the structured summary + markdown report.
|
|
# suggestedComment is kept as a "Yes"/"No" string for dashboard compatibility.
|
|
PAYLOAD=$(jq \
|
|
--arg githubUrl "https://github.com/${REPO}/issues/${ISSUE}" \
|
|
--rawfile reportMarkdown "data/${ISSUE}/report.md" \
|
|
'{
|
|
title: .title,
|
|
githubUrl: $githubUrl,
|
|
recommendation: .recommendation,
|
|
difficulty: .difficulty,
|
|
reasoning: .reasoning,
|
|
suggestedAction: .suggestedAction,
|
|
suggestedComment: (if .hasSuggestedComment then "Yes" else "No" end),
|
|
reportMarkdown: $reportMarkdown
|
|
}' "data/${ISSUE}/summary.json")
|
|
|
|
# POST to dashboard API
|
|
HTTP_STATUS=$(curl -L --post302 -s -o response.json -w "%{http_code}" \
|
|
-X POST "${DASHBOARD_URL}/api/report/${ISSUE}" \
|
|
-H "Content-Type: application/json" \
|
|
-H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \
|
|
-H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \
|
|
-d "$PAYLOAD")
|
|
|
|
echo "Dashboard API response: HTTP ${HTTP_STATUS}"
|
|
cat response.json
|
|
|
|
if [ "$HTTP_STATUS" != "200" ]; then
|
|
echo "::error::Failed to upload report (HTTP ${HTTP_STATUS})"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Report uploaded for issue #${ISSUE}"
|
|
|
|
- name: Build triage comment
|
|
env:
|
|
ISSUE: ${{ steps.issue.outputs.number }}
|
|
run: |
|
|
{
|
|
echo "> [!NOTE]"
|
|
echo "> This is an automated, advisory triage report generated by workers-devprod. It is not an official maintainer response — a maintainer will follow up."
|
|
echo ""
|
|
echo "<details>"
|
|
echo "<summary>🤖 Automated triage report</summary>"
|
|
echo ""
|
|
cat "data/${ISSUE}/report.md"
|
|
echo ""
|
|
echo "</details>"
|
|
echo ""
|
|
echo "<details>"
|
|
echo "<summary>Structured summary (JSON)</summary>"
|
|
echo ""
|
|
echo '```json'
|
|
cat "data/${ISSUE}/summary.json"
|
|
echo ""
|
|
echo '```'
|
|
echo ""
|
|
echo "</details>"
|
|
} > "data/${ISSUE}/comment.md"
|
|
|
|
- name: Post triage comment
|
|
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
|
|
with:
|
|
header: triage-report
|
|
number: ${{ steps.issue.outputs.number }}
|
|
path: data/${{ steps.issue.outputs.number }}/comment.md
|
|
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
|
|
- name: Apply suggested labels
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
ISSUE: ${{ steps.issue.outputs.number }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
SUMMARY_PATH="data/${ISSUE}/summary.json"
|
|
if [ ! -f "$SUMMARY_PATH" ]; then
|
|
echo "No summary.json; skipping label application"
|
|
exit 0
|
|
fi
|
|
|
|
# suggestedLabels is a JSON array; emit one label per line.
|
|
jq -r '.suggestedLabels // [] | .[]' "$SUMMARY_PATH" > suggested-labels.txt
|
|
if [ ! -s suggested-labels.txt ]; then
|
|
echo "No suggested labels"
|
|
exit 0
|
|
fi
|
|
|
|
# Only apply labels that actually exist in the repo.
|
|
gh label list --repo "$REPO" --limit 500 --json name --jq '.[].name' > repo-labels.txt
|
|
|
|
ARGS=()
|
|
while IFS= read -r label; do
|
|
if [ -z "$label" ]; then
|
|
continue
|
|
fi
|
|
if grep -Fxq "$label" repo-labels.txt; then
|
|
ARGS+=(--add-label "$label")
|
|
else
|
|
echo "Skipping unknown label: ${label}"
|
|
fi
|
|
done < suggested-labels.txt
|
|
|
|
if [ ${#ARGS[@]} -eq 0 ]; then
|
|
echo "No valid labels to apply"
|
|
exit 0
|
|
fi
|
|
|
|
gh issue edit "$ISSUE" --repo "$REPO" "${ARGS[@]}"
|