mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
296 lines
12 KiB
YAML
296 lines
12 KiB
YAML
# 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}"
|