Agents: Integrate with external issue trackers #235
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
| # AI Triage Automation using the Coder Chat API (/api/v2/chats): | |
| # create a chat for the labeled issue, poll until the agent finishes, and | |
| # link the resulting chat on the issue. All API calls use curl + jq directly. | |
| 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.label.name == 'traiage' || 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/v2/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}" | |
| # Chat creation requires an organization; use the deployment's | |
| # default one, matching the old workflow's "default" organization. | |
| ORG_ID=$(curl --silent --fail-with-body \ | |
| -H "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \ | |
| "${CODER_URL}/api/v2/organizations" | jq -r '.[] | select(.is_default) | .id') | |
| if [[ -z "${ORG_ID}" || "${ORG_ID}" == "null" ]]; then | |
| echo "::error::Failed to resolve the default organization" | |
| exit 1 | |
| fi | |
| # 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}" --arg org "${ORG_ID}" \ | |
| '{organization_id: $org, content: [{type: "text", text: $prompt}]}')" \ | |
| "${CODER_URL}/api/v2/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_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/v2/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/v2/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: always() && steps.create-chat.outputs.chat_id != '' && 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 || 'unknown' }} | |
| 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. | |
| _The chat runs under the Coder triage service account; the Chats API does not support creating chats on behalf of the triggering user._ | |
| 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 | |
| if: always() && steps.create-chat.outputs.chat_id != '' | |
| 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 || 'unknown' }} | |
| 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}" |