# This workflow checks if a PR requires documentation updates. # It creates a Coder Task that uses AI to analyze the PR changes, # search existing docs, and comment with recommendations. # # 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 template_preset: description: "Template preset to use" required: false default: "" 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: Setup Coder CLI if: steps.check-secrets.outputs.skip != 'true' uses: coder/setup-action@4a607a8113d4e676e2d7c34caa20a814bc88bfda # v1 with: access_url: ${{ secrets.DOC_CHECK_CODER_URL }} coder_session_token: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} - 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 }} INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || '' }} run: | echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}" echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}" # Determine trigger type for task 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 task 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 task prompt with sticky comment logic TASK_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 Tasks](https://coder.com/docs/ai-coder/tasks)* \`\`\` The \`\` marker must be at the end so future runs can find and update this comment." # Output the prompt { echo "task_prompt<> "${GITHUB_OUTPUT}" - name: Checkout create-task-action if: steps.check-secrets.outputs.skip != 'true' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 path: ./.github/actions/create-task-action persist-credentials: false ref: main repository: coder/create-task-action - name: Create Coder Task for Documentation Check if: steps.check-secrets.outputs.skip != 'true' id: create_task continue-on-error: true uses: ./.github/actions/create-task-action with: coder-url: ${{ secrets.DOC_CHECK_CODER_URL }} coder-token: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} coder-organization: "default" coder-template-name: coder-workflow-bot coder-template-preset: ${{ steps.determine-context.outputs.template_preset }} coder-task-name-prefix: doc-check coder-task-prompt: ${{ steps.extract-context.outputs.task_prompt }} coder-username: doc-check-bot github-token: ${{ github.token }} github-issue-url: ${{ steps.determine-context.outputs.pr_url }} comment-on-issue: false - name: Handle Task Creation Failure if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome != 'success' run: | { echo "## Documentation Check Task" echo "" echo "⚠️ The external Coder task service 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 Task Info if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success' env: TASK_CREATED: ${{ steps.create_task.outputs.task-created }} TASK_NAME: ${{ steps.create_task.outputs.task-name }} TASK_URL: ${{ steps.create_task.outputs.task-url }} PR_URL: ${{ steps.determine-context.outputs.pr_url }} run: | { echo "## Documentation Check Task" echo "" echo "**PR:** ${PR_URL}" echo "**Task created:** ${TASK_CREATED}" echo "**Task name:** ${TASK_NAME}" echo "**Task URL:** ${TASK_URL}" echo "" } >> "${GITHUB_STEP_SUMMARY}" - name: Wait for Task Completion if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success' id: wait_task env: TASK_NAME: ${{ steps.create_task.outputs.task-name }} run: | echo "Waiting for task to complete..." echo "Task name: ${TASK_NAME}" if [[ -z "${TASK_NAME}" ]]; then echo "::error::TASK_NAME is empty" exit 1 fi MAX_WAIT=600 # 10 minutes WAITED=0 POLL_INTERVAL=3 LAST_STATUS="" is_workspace_message() { local msg="$1" [[ -z "$msg" ]] && return 0 # Empty = treat as workspace/startup [[ "$msg" =~ ^Workspace ]] && return 0 [[ "$msg" =~ ^Agent ]] && return 0 return 1 } while [[ $WAITED -lt $MAX_WAIT ]]; do # Get task status (|| true prevents set -e from exiting on non-zero) RAW_OUTPUT=$(coder task status "${TASK_NAME}" -o json 2>&1) || true STATUS_JSON=$(echo "$RAW_OUTPUT" | grep -v "^version mismatch\|^download v" || true) # Debug: show first poll's raw output if [[ $WAITED -eq 0 ]]; then echo "Raw status output: ${RAW_OUTPUT:0:500}" fi if [[ -z "$STATUS_JSON" ]] || ! echo "$STATUS_JSON" | jq -e . >/dev/null 2>&1; then if [[ "$LAST_STATUS" != "waiting" ]]; then echo "[${WAITED}s] Waiting for task status..." LAST_STATUS="waiting" fi sleep $POLL_INTERVAL WAITED=$((WAITED + POLL_INTERVAL)) continue fi TASK_STATE=$(echo "$STATUS_JSON" | jq -r '.current_state.state // "unknown"') TASK_MESSAGE=$(echo "$STATUS_JSON" | jq -r '.current_state.message // ""') WORKSPACE_STATUS=$(echo "$STATUS_JSON" | jq -r '.workspace_status // "unknown"') # Build current status string for comparison CURRENT_STATUS="${TASK_STATE}|${WORKSPACE_STATUS}|${TASK_MESSAGE}" # Only log if status changed if [[ "$CURRENT_STATUS" != "$LAST_STATUS" ]]; then if [[ "$TASK_STATE" == "idle" ]] && is_workspace_message "$TASK_MESSAGE"; then echo "[${WAITED}s] Workspace ready, waiting for Agent..." else echo "[${WAITED}s] State: ${TASK_STATE} | Workspace: ${WORKSPACE_STATUS} | ${TASK_MESSAGE}" fi LAST_STATUS="$CURRENT_STATUS" fi if [[ "$WORKSPACE_STATUS" == "failed" || "$WORKSPACE_STATUS" == "canceled" ]]; then echo "::error::Workspace failed: ${WORKSPACE_STATUS}" exit 1 fi if [[ "$TASK_STATE" == "idle" ]]; then if ! is_workspace_message "$TASK_MESSAGE"; then # Real completion message from Claude! echo "" echo "Task completed: ${TASK_MESSAGE}" RESULT_URI=$(echo "$STATUS_JSON" | jq -r '.current_state.uri // ""') echo "result_uri=${RESULT_URI}" >> "${GITHUB_OUTPUT}" echo "task_message=${TASK_MESSAGE}" >> "${GITHUB_OUTPUT}" break fi fi sleep $POLL_INTERVAL WAITED=$((WAITED + POLL_INTERVAL)) done if [[ $WAITED -ge $MAX_WAIT ]]; then echo "::error::Task monitoring timed out after ${MAX_WAIT}s" exit 1 fi - name: Fetch Task Logs if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success' env: TASK_NAME: ${{ steps.create_task.outputs.task-name }} run: | echo "::group::Task Conversation Log" if [[ -n "${TASK_NAME}" ]]; then coder task logs "${TASK_NAME}" 2>&1 || echo "Failed to fetch logs" else echo "No task name, skipping log fetch" fi echo "::endgroup::" - name: Cleanup Task if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success' env: TASK_NAME: ${{ steps.create_task.outputs.task-name }} run: | if [[ -n "${TASK_NAME}" ]]; then echo "Deleting task: ${TASK_NAME}" coder task delete "${TASK_NAME}" -y 2>&1 || echo "Task deletion failed or already deleted" else echo "No task name, skipping cleanup" fi - name: Write Final Summary if: always() && steps.check-secrets.outputs.skip != 'true' env: CREATE_TASK_OUTCOME: ${{ steps.create_task.outcome }} TASK_NAME: ${{ steps.create_task.outputs.task-name }} TASK_MESSAGE: ${{ steps.wait_task.outputs.task_message }} RESULT_URI: ${{ steps.wait_task.outputs.result_uri }} PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }} run: | { echo "" echo "---" echo "### Result" echo "" if [[ "${CREATE_TASK_OUTCOME}" == "success" ]]; then echo "**Status:** ${TASK_MESSAGE:-Task completed}" if [[ -n "${RESULT_URI}" ]]; then echo "**Comment:** ${RESULT_URI}" fi echo "" echo "Task \`${TASK_NAME}\` has been cleaned up." else echo "**Status:** Skipped because the external Coder task" echo "service was unavailable." fi } >> "${GITHUB_STEP_SUMMARY}"