# 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 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: Write Task Info if: steps.check-secrets.outputs.skip != 'true' 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' 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' 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' 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: 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 "" 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." } >> "${GITHUB_STEP_SUMMARY}"