# This workflow checks if a PR requires documentation updates. # It creates a Coder Agent chat session that uses AI to analyze the PR # changes, search existing docs, and comment with recommendations. # # Uses the Coder Chat API (/api/experimental/chats) instead of the Tasks # API — all API calls use curl + jq directly, no dedicated GitHub Action # or workspace provisioning required. # # Triggers: # - New PR opened: Initial documentation review # - PR updated (synchronize): Re-review after changes # - Label "doc-check" added: Manual trigger for review # - PR marked ready for review: Review when draft is promoted # - Workflow dispatch: Manual run with PR URL # # Note: This workflow requires access to secrets and will be skipped for: # - Any PR where secrets are not available # For these PRs, maintainers can manually trigger via workflow_dispatch. name: AI Documentation Check on: pull_request: types: - opened - synchronize - labeled - ready_for_review workflow_dispatch: inputs: pr_url: description: "Pull Request URL to check" required: true type: string permissions: contents: read jobs: doc-check: name: Analyze PR for Documentation Updates Needed runs-on: ubuntu-latest # Run on: opened, synchronize, labeled (with doc-check label), ready_for_review, or workflow_dispatch # Skip draft PRs unless manually triggered if: | ( github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.label.name == 'doc-check' || github.event.action == 'ready_for_review' || github.event_name == 'workflow_dispatch' ) && (github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch') timeout-minutes: 30 env: CODER_URL: ${{ secrets.DOC_CHECK_CODER_URL }} CODER_SESSION_TOKEN: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} permissions: contents: read pull-requests: write steps: - name: Check if secrets are available id: check-secrets env: CODER_URL: ${{ secrets.DOC_CHECK_CODER_URL }} CODER_TOKEN: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} run: | if [[ -z "${CODER_URL}" || -z "${CODER_TOKEN}" ]]; then echo "skip=true" >> "${GITHUB_OUTPUT}" echo "Secrets not available - skipping doc-check." echo "This is expected for PRs where secrets are not available." echo "Maintainers can manually trigger via workflow_dispatch if needed." { echo "⚠️ Workflow skipped: Secrets not available" echo "" echo "This workflow requires secrets that are unavailable for this run." echo "Maintainers can manually trigger via workflow_dispatch if needed." } >> "${GITHUB_STEP_SUMMARY}" else echo "skip=false" >> "${GITHUB_OUTPUT}" fi - name: Determine PR Context if: steps.check-secrets.outputs.skip != 'true' id: determine-context env: GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_ACTION: ${{ github.event.action }} GITHUB_EVENT_PR_HTML_URL: ${{ github.event.pull_request.html_url }} GITHUB_EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} INPUTS_PR_URL: ${{ inputs.pr_url }} run: | # Determine trigger type for context if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then echo "trigger_type=manual" >> "${GITHUB_OUTPUT}" echo "Using PR URL: ${INPUTS_PR_URL}" # Validate PR URL format if [[ ! "${INPUTS_PR_URL}" =~ ^https://github\.com/[^/]+/[^/]+/pull/[0-9]+$ ]]; then echo "::error::Invalid PR URL format: ${INPUTS_PR_URL}" echo "::error::Expected format: https://github.com/owner/repo/pull/NUMBER" exit 1 fi ISSUE_URL="${INPUTS_PR_URL/\/pull\//\/issues\/}" echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}" PR_NUMBER=$(echo "${INPUTS_PR_URL}" | grep -oP '(?<=pull/)\d+') echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then echo "Using PR URL: ${GITHUB_EVENT_PR_HTML_URL}" ISSUE_URL="${GITHUB_EVENT_PR_HTML_URL/\/pull\//\/issues\/}" echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}" echo "pr_number=${GITHUB_EVENT_PR_NUMBER}" >> "${GITHUB_OUTPUT}" # Set trigger type based on action case "${GITHUB_EVENT_ACTION}" in opened) echo "trigger_type=new_pr" >> "${GITHUB_OUTPUT}" ;; synchronize) echo "trigger_type=pr_updated" >> "${GITHUB_OUTPUT}" ;; labeled) echo "trigger_type=label_requested" >> "${GITHUB_OUTPUT}" ;; ready_for_review) echo "trigger_type=ready_for_review" >> "${GITHUB_OUTPUT}" ;; *) echo "trigger_type=unknown" >> "${GITHUB_OUTPUT}" ;; esac else echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}" exit 1 fi - name: Build chat prompt if: steps.check-secrets.outputs.skip != 'true' id: extract-context env: PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }} TRIGGER_TYPE: ${{ steps.determine-context.outputs.trigger_type }} run: | echo "Analyzing PR #${PR_NUMBER} (trigger: ${TRIGGER_TYPE})" # Build context based on trigger type case "${TRIGGER_TYPE}" in new_pr) CONTEXT="This is a NEW PR. Perform initial documentation review." ;; pr_updated) CONTEXT="This PR was UPDATED with new commits. Check if previous feedback was addressed or if new doc needs arose." ;; label_requested) CONTEXT="A documentation review was REQUESTED via label. Perform a thorough review." ;; ready_for_review) CONTEXT="This PR was marked READY FOR REVIEW. Perform a thorough review." ;; manual) CONTEXT="This is a MANUAL review request. Perform a thorough review." ;; *) CONTEXT="Perform a documentation review." ;; esac # Build chat prompt with sticky comment logic CHAT_PROMPT="Use the doc-check skill to review PR #${PR_NUMBER} in coder/coder. ${CONTEXT} Use \`gh\` to get PR details, diff, and all comments. Look for an existing doc-check comment containing \`\` - if one exists, you'll update it instead of creating a new one. **Do not comment if no documentation changes are needed.** If a sticky comment already exists, compare your current findings against it: - Check off \`[x]\` items that are now addressed - Strikethrough items no longer needed (e.g., code was reverted) - Add new unchecked \`[ ]\` items for newly discovered needs - If an item is checked but you can't verify the docs were added, add a warning note below it - If nothing meaningful changed, don't update the comment at all ## Comment format Use this structure (only include relevant sections): \`\`\` ## Documentation Check ### Updates Needed - [ ] \`docs/path/file.md\` - What needs to change - [x] \`docs/other/file.md\` - This was addressed - ~~\`docs/removed.md\` - No longer needed~~ *(reverted in abc123)* ### New Documentation Needed - [ ] \`docs/suggested/path.md\` - What should be documented > ⚠️ *Checked but no corresponding documentation changes found in this PR* --- *Automated review via [Coder Agents](https://coder.com/docs/ai-coder/agents)* \`\`\` The \`\` marker must be at the end so future runs can find and update this comment." # Output the prompt { echo "chat_prompt<> "${GITHUB_OUTPUT}" # ------------------------------------------------------------------ # Create a chat via the Coder Chat API. # The Chat API creates a lightweight chat session — no workspace # provisioning or dedicated GitHub Action checkout required. # ------------------------------------------------------------------ - name: Create chat via Coder Chat API if: steps.check-secrets.outputs.skip != 'true' id: create-chat continue-on-error: true env: CHAT_PROMPT: ${{ steps.extract-context.outputs.chat_prompt }} run: | set -euo pipefail echo "Creating 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 "${CHAT_PROMPT}" \ '{content: [{type: "text", text: $prompt}]}')" \ "${CODER_URL}/api/experimental/chats") 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. 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}" - name: Handle Chat Creation Failure if: steps.check-secrets.outputs.skip != 'true' && steps.create-chat.outcome != 'success' run: | { echo "## Documentation Check" echo "" echo "⚠️ The Coder Chat API was unavailable, so this" echo "advisory documentation check did not run." echo "" echo "Maintainers can rerun the workflow or trigger it manually" echo "after the service recovers." } >> "${GITHUB_STEP_SUMMARY}" - name: Write Chat Info if: steps.check-secrets.outputs.skip != 'true' && steps.create-chat.outcome == 'success' env: CHAT_ID: ${{ steps.create-chat.outputs.chat_id }} CHAT_URL: ${{ steps.create-chat.outputs.chat_url }} PR_URL: ${{ steps.determine-context.outputs.pr_url }} run: | { echo "## Documentation Check" echo "" echo "**PR:** ${PR_URL}" echo "**Chat ID:** \`${CHAT_ID}\`" echo "**Chat URL:** ${CHAT_URL}" echo "" } >> "${GITHUB_STEP_SUMMARY}" # ------------------------------------------------------------------ # 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/ # every 5 seconds until the status is "waiting" (agent needs input), # "completed" (agent finished), or "error". Timeout after 10 minutes. # ------------------------------------------------------------------ - name: Poll chat status if: steps.check-secrets.outputs.skip != 'true' && steps.create-chat.outcome == 'success' id: poll-status env: CHAT_ID: ${{ steps.create-chat.outputs.chat_id }} run: | set -euo pipefail POLL_INTERVAL=5 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 - name: Write Final Summary if: always() && steps.check-secrets.outputs.skip != 'true' env: CREATE_CHAT_OUTCOME: ${{ steps.create-chat.outcome }} CHAT_ID: ${{ steps.create-chat.outputs.chat_id }} CHAT_URL: ${{ steps.create-chat.outputs.chat_url }} FINAL_STATUS: ${{ steps.poll-status.outputs.final_status }} PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }} run: | { echo "" echo "---" echo "### Result" echo "" if [[ "${CREATE_CHAT_OUTCOME}" == "success" ]]; then echo "**Status:** ${FINAL_STATUS:-Chat completed}" echo "**Chat URL:** ${CHAT_URL}" echo "" echo "Chat \`${CHAT_ID}\` has finished." else echo "**Status:** Skipped because the Coder Chat API" echo "was unavailable." fi } >> "${GITHUB_STEP_SUMMARY}"