mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
ci: add triage workflow using Coder Chat API (#23154)
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# This workflow reimplements the AI Triage Automation using the Coder Chat API
|
||||
# instead of the Tasks API. The Chat API (/api/experimental/chats) is a simpler
|
||||
# interface that does not require a dedicated GitHub Action or workspace
|
||||
# provisioning — we just create a chat, poll for completion, and link the
|
||||
# result on the issue. All API calls use curl + jq directly.
|
||||
#
|
||||
# Key differences from the Tasks API workflow (traiage.yaml):
|
||||
# - No checkout of coder/create-task-action; everything is inline curl/jq.
|
||||
# - No template_name / template_preset / prefix inputs — the Chat API handles
|
||||
# resource allocation internally.
|
||||
# - Uses POST /api/experimental/chats to create a chat session.
|
||||
# - Polls GET /api/experimental/chats/<id> until the agent finishes.
|
||||
# - Chat URL format: ${CODER_URL}/agents?chat=${CHAT_ID}
|
||||
|
||||
name: AI Triage via Chat API
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_url:
|
||||
description: "GitHub Issue URL to process"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
triage-chat:
|
||||
name: Triage GitHub Issue via Chat API
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'chat-triage' || github.event_name == 'workflow_dispatch'
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CODER_URL: ${{ secrets.TRAIAGE_CODER_URL }}
|
||||
CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Determine the GitHub user and issue URL.
|
||||
# Identical to the Tasks API workflow — resolve the actor for
|
||||
# workflow_dispatch or the issue sender for label events.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Determine Inputs
|
||||
id: determine-inputs
|
||||
if: always()
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_EVENT_ISSUE_HTML_URL: ${{ github.event.issue.html_url }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_EVENT_USER_ID: ${{ github.event.sender.id }}
|
||||
GITHUB_EVENT_USER_LOGIN: ${{ github.event.sender.login }}
|
||||
INPUTS_ISSUE_URL: ${{ inputs.issue_url }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# For workflow_dispatch, use the actor who triggered it.
|
||||
# For issues events, use the issue sender.
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
||||
if ! GITHUB_USER_ID=$(gh api "users/${GITHUB_ACTOR}" --jq '.id'); then
|
||||
echo "::error::Failed to get GitHub user ID for actor ${GITHUB_ACTOR}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using workflow_dispatch actor: ${GITHUB_ACTOR} (ID: ${GITHUB_USER_ID})"
|
||||
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
||||
echo "github_username=${GITHUB_ACTOR}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using issue URL: ${INPUTS_ISSUE_URL}"
|
||||
echo "issue_url=${INPUTS_ISSUE_URL}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
exit 0
|
||||
elif [[ "${GITHUB_EVENT_NAME}" == "issues" ]]; then
|
||||
GITHUB_USER_ID=${GITHUB_EVENT_USER_ID}
|
||||
echo "Using issue author: ${GITHUB_EVENT_USER_LOGIN} (ID: ${GITHUB_USER_ID})"
|
||||
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
||||
echo "github_username=${GITHUB_EVENT_USER_LOGIN}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using issue URL: ${GITHUB_EVENT_ISSUE_HTML_URL}"
|
||||
echo "issue_url=${GITHUB_EVENT_ISSUE_HTML_URL}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Verify the triggering user has push access.
|
||||
# Unchanged from the Tasks API workflow.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Verify push access
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_USERNAME: ${{ steps.determine-inputs.outputs.github_username }}
|
||||
GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
can_push="$(gh api "/repos/${GITHUB_REPOSITORY}/collaborators/${GITHUB_USERNAME}/permission" --jq '.user.permissions.push')"
|
||||
if [[ "${can_push}" != "true" ]]; then
|
||||
echo "::error title=Access Denied::${GITHUB_USERNAME} does not have push access to ${GITHUB_REPOSITORY}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Create a chat via the Coder Chat API.
|
||||
# Unlike the Tasks API which provisions a full workspace, the Chat
|
||||
# API creates a lightweight chat session. We POST to
|
||||
# /api/experimental/chats with the triage prompt as the initial
|
||||
# message and receive a chat ID back.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create chat via Coder Chat API
|
||||
id: create-chat
|
||||
env:
|
||||
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Build the same triage prompt used by the Tasks API workflow.
|
||||
TASK_PROMPT=$(cat <<'EOF'
|
||||
Fix ${ISSUE_URL}
|
||||
|
||||
1. Use the gh CLI to read the issue description and comments.
|
||||
2. Think carefully and try to understand the root cause. If the issue is unclear or not well defined, ask me to clarify and provide more information.
|
||||
3. Write a proposed implementation plan to PLAN.md for me to review before starting implementation. Your plan should use TDD and only make the minimal changes necessary to fix the root cause.
|
||||
4. When I approve your plan, start working on it. If you encounter issues with the plan, ask me for clarification and update the plan as required.
|
||||
5. When you have finished implementation according to the plan, commit and push your changes, and create a PR using the gh CLI for me to review.
|
||||
EOF
|
||||
)
|
||||
# Perform variable substitution on the prompt — scoped to $ISSUE_URL only.
|
||||
# Using envsubst without arguments would expand every env var in scope
|
||||
# (including CODER_SESSION_TOKEN), so we name the variable explicitly.
|
||||
TASK_PROMPT=$(echo "${TASK_PROMPT}" | envsubst '$ISSUE_URL')
|
||||
|
||||
echo "Creating chat with prompt:"
|
||||
echo "${TASK_PROMPT}"
|
||||
|
||||
# POST to the Chat API to create a new chat session.
|
||||
RESPONSE=$(curl --silent --fail-with-body \
|
||||
-X POST \
|
||||
-H "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg prompt "${TASK_PROMPT}" \
|
||||
'{content: [{type: "text", text: $prompt}]}')" \
|
||||
"${CODER_URL}/api/experimental/chats")
|
||||
|
||||
echo "Chat API response:"
|
||||
echo "${RESPONSE}" | jq .
|
||||
|
||||
CHAT_ID=$(echo "${RESPONSE}" | jq -r '.id')
|
||||
CHAT_STATUS=$(echo "${RESPONSE}" | jq -r '.status')
|
||||
|
||||
if [[ -z "${CHAT_ID}" || "${CHAT_ID}" == "null" ]]; then
|
||||
echo "::error::Failed to create chat — no ID returned"
|
||||
echo "Response: ${RESPONSE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that CHAT_ID is a UUID before using it in URL paths.
|
||||
# This guards against unexpected API responses being interpolated
|
||||
# into subsequent curl calls.
|
||||
if [[ ! "${CHAT_ID}" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
||||
echo "::error::CHAT_ID is not a valid UUID: ${CHAT_ID}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHAT_URL="${CODER_URL}/agents?chat=${CHAT_ID}"
|
||||
|
||||
echo "Chat created: ${CHAT_ID} (status: ${CHAT_STATUS})"
|
||||
echo "Chat URL: ${CHAT_URL}"
|
||||
|
||||
echo "chat_id=${CHAT_ID}" >> "${GITHUB_OUTPUT}"
|
||||
echo "chat_url=${CHAT_URL}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Poll the chat status until the agent finishes.
|
||||
# The Chat API is asynchronous — after creation the agent begins
|
||||
# working in the background. We poll GET /api/experimental/chats/<id>
|
||||
# every 5 seconds until the status is "waiting" (agent needs input),
|
||||
# "completed" (agent finished), or "error". Timeout after 10 minutes.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Poll chat status
|
||||
id: poll-status
|
||||
env:
|
||||
CHAT_ID: ${{ steps.create-chat.outputs.chat_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
POLL_INTERVAL=5
|
||||
# 10 minutes = 600 seconds.
|
||||
TIMEOUT=600
|
||||
ELAPSED=0
|
||||
|
||||
echo "Polling chat ${CHAT_ID} every ${POLL_INTERVAL}s (timeout: ${TIMEOUT}s)..."
|
||||
|
||||
while true; do
|
||||
RESPONSE=$(curl --silent --fail-with-body \
|
||||
-H "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \
|
||||
"${CODER_URL}/api/experimental/chats/${CHAT_ID}")
|
||||
|
||||
STATUS=$(echo "${RESPONSE}" | jq -r '.status')
|
||||
|
||||
echo "[${ELAPSED}s] Chat status: ${STATUS}"
|
||||
|
||||
case "${STATUS}" in
|
||||
waiting|completed)
|
||||
echo "Chat reached terminal status: ${STATUS}"
|
||||
echo "final_status=${STATUS}" >> "${GITHUB_OUTPUT}"
|
||||
exit 0
|
||||
;;
|
||||
error)
|
||||
echo "::error::Chat entered error state"
|
||||
echo "${RESPONSE}" | jq .
|
||||
echo "final_status=error" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
;;
|
||||
pending|running)
|
||||
# Still working — keep polling.
|
||||
;;
|
||||
*)
|
||||
echo "::warning::Unknown chat status: ${STATUS}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ${ELAPSED} -ge ${TIMEOUT} ]]; then
|
||||
echo "::error::Timed out after ${TIMEOUT}s waiting for chat to finish"
|
||||
echo "final_status=timeout" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "${POLL_INTERVAL}"
|
||||
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
||||
done
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Comment on the GitHub issue with a link to the chat.
|
||||
# Only comment if the issue belongs to this repository (same guard
|
||||
# as the Tasks API workflow).
|
||||
# ------------------------------------------------------------------
|
||||
- name: Comment on issue
|
||||
if: startsWith(steps.determine-inputs.outputs.issue_url, format('{0}/{1}', github.server_url, github.repository))
|
||||
env:
|
||||
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
CHAT_URL: ${{ steps.create-chat.outputs.chat_url }}
|
||||
CHAT_ID: ${{ steps.create-chat.outputs.chat_id }}
|
||||
FINAL_STATUS: ${{ steps.poll-status.outputs.final_status }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
COMMENT_BODY=$(cat <<EOF
|
||||
🤖 **AI Triage Chat Created**
|
||||
|
||||
A Coder chat session has been created to investigate this issue.
|
||||
|
||||
**Chat URL:** ${CHAT_URL}
|
||||
**Chat ID:** \`${CHAT_ID}\`
|
||||
**Status:** ${FINAL_STATUS}
|
||||
|
||||
The agent is working on a triage plan. Visit the chat to follow progress or provide guidance.
|
||||
EOF
|
||||
)
|
||||
|
||||
gh issue comment "${ISSUE_URL}" --body "${COMMENT_BODY}"
|
||||
echo "Comment posted on ${ISSUE_URL}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 6: Write a summary to the GitHub Actions step summary.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Write summary
|
||||
env:
|
||||
CHAT_ID: ${{ steps.create-chat.outputs.chat_id }}
|
||||
CHAT_URL: ${{ steps.create-chat.outputs.chat_url }}
|
||||
FINAL_STATUS: ${{ steps.poll-status.outputs.final_status }}
|
||||
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
{
|
||||
echo "## AI Triage via Chat API"
|
||||
echo ""
|
||||
echo "**Issue:** ${ISSUE_URL}"
|
||||
echo "**Chat ID:** \`${CHAT_ID}\`"
|
||||
echo "**Chat URL:** ${CHAT_URL}"
|
||||
echo "**Status:** ${FINAL_STATUS}"
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
Reference in New Issue
Block a user