feat: add CLI commands for managing chat context from workspaces #15127
Workflow file for this run
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
| # 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 \`<!-- doc-check-sticky -->\` - 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)* | |
| <!-- doc-check-sticky --> | |
| \`\`\` | |
| The \`<!-- doc-check-sticky -->\` marker must be at the end so future runs can find and update this comment." | |
| # Output the prompt | |
| { | |
| echo "task_prompt<<EOFOUTPUT" | |
| echo "${TASK_PROMPT}" | |
| echo "EOFOUTPUT" | |
| } >> "${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}" |