diff --git a/.claude/skills/doc-check/SKILL.md b/.claude/skills/doc-check/SKILL.md new file mode 100644 index 0000000000..fcfde8d28c --- /dev/null +++ b/.claude/skills/doc-check/SKILL.md @@ -0,0 +1,79 @@ +--- +name: doc-check +description: Checks if code changes require documentation updates +--- + +# Documentation Check Skill + +Review code changes and determine if documentation updates or new documentation +is needed. + +## Workflow + +1. **Get the code changes** - Use the method provided in the prompt, or if none + specified: + - For a PR: `gh pr diff --repo coder/coder` + - For local changes: `git diff main` or `git diff --staged` + - For a branch: `git diff main...` + +2. **Understand the scope** - Consider what changed: + - Is this user-facing or internal? + - Does it change behavior, APIs, CLI flags, or configuration? + - Even for "internal" or "chore" changes, always verify the actual diff + +3. **Search the docs** for related content in `docs/` + +4. **Decide what's needed**: + - Do existing docs need updates to match the code? + - Is new documentation needed for undocumented features? + - Or is everything already covered? + +5. **Report findings** - Use the method provided in the prompt, or if none + specified, summarize findings directly + +## What to Check + +- **Accuracy**: Does documentation match current code behavior? +- **Completeness**: Are new features/options documented? +- **Examples**: Do code examples still work? +- **CLI/API changes**: Are new flags, endpoints, or options documented? +- **Configuration**: Are new environment variables or settings documented? +- **Breaking changes**: Are migration steps documented if needed? +- **Premium features**: Should docs indicate `(Premium)` in the title? + +## Key Documentation Info + +- **`docs/manifest.json`** - Navigation structure; new pages MUST be added here +- **`docs/reference/cli/*.md`** - Auto-generated from Go code, don't edit directly +- **Premium features** - H1 title should include `(Premium)` suffix + +## Coder-Specific Patterns + +### Callouts + +Use GitHub-Flavored Markdown alerts: + +```markdown +> [!NOTE] +> Additional helpful information. + +> [!WARNING] +> Important warning about potential issues. + +> [!TIP] +> Helpful tip for users. +``` + +### CLI Documentation + +CLI docs in `docs/reference/cli/` are auto-generated. Don't suggest editing them +directly. Instead, changes should be made in the Go code that defines the CLI +commands (typically in `cli/` directory). + +### Code Examples + +Use `sh` for shell commands: + +```sh +coder server --flag-name value +``` diff --git a/.github/workflows/doc-check.yaml b/.github/workflows/doc-check.yaml index 2657b2653d..8be132681d 100644 --- a/.github/workflows/doc-check.yaml +++ b/.github/workflows/doc-check.yaml @@ -2,13 +2,19 @@ # It creates a Coder Task that uses AI to analyze the PR changes, # search existing docs, and comment with recommendations. # -# Triggered by: Adding the "doc-check" label to a PR, or manual dispatch. +# Triggers: +# - New PR opened: Initial documentation review +# - PR updated (synchronize): Re-review after changes +# - Label "doc-check" added: Manual trigger for review +# - Workflow dispatch: Manual run with PR URL name: AI Documentation Check on: pull_request: types: + - opened + - synchronize - labeled workflow_dispatch: inputs: @@ -26,8 +32,15 @@ jobs: doc-check: name: Analyze PR for Documentation Updates Needed runs-on: ubuntu-latest + # Run on: opened, synchronize, labeled (with doc-check label), or workflow_dispatch + # Skip draft PRs unless manually triggered if: | - (github.event.label.name == 'doc-check' || github.event_name == 'workflow_dispatch') && + ( + github.event.action == 'opened' || + github.event.action == 'synchronize' || + github.event.label.name == 'doc-check' || + github.event_name == 'workflow_dispatch' + ) && (github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch') timeout-minutes: 30 env: @@ -39,120 +52,125 @@ jobs: actions: write steps: + - name: Setup Coder CLI + 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 id: determine-context env: - GITHUB_ACTOR: ${{ github.actor }} 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 }} - GITHUB_EVENT_SENDER_ID: ${{ github.event.sender.id }} - GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }} INPUTS_PR_URL: ${{ inputs.pr_url }} INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || '' }} - GH_TOKEN: ${{ github.token }} run: | echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}" echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}" - # For workflow_dispatch, use the provided PR URL + # Determine trigger type for task context 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}" + 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 - 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 PR URL: ${INPUTS_PR_URL}" - # Convert /pull/ to /issues/ for create-task-action compatibility ISSUE_URL="${INPUTS_PR_URL/\/pull\//\/issues\/}" echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}" - - # Extract PR number from URL for later use PR_NUMBER=$(echo "${INPUTS_PR_URL}" | grep -oP '(?<=pull/)\d+') echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - GITHUB_USER_ID=${GITHUB_EVENT_SENDER_ID} - echo "Using label adder: ${GITHUB_EVENT_SENDER_LOGIN} (ID: ${GITHUB_USER_ID})" - echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}" - echo "github_username=${GITHUB_EVENT_SENDER_LOGIN}" >> "${GITHUB_OUTPUT}" - echo "Using PR URL: ${GITHUB_EVENT_PR_HTML_URL}" - # Convert /pull/ to /issues/ for create-task-action compatibility 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}" + ;; + *) + echo "trigger_type=unknown" >> "${GITHUB_OUTPUT}" + ;; + esac + else echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}" exit 1 fi - - name: Extract changed files and build prompt + - name: Build task prompt id: extract-context env: - PR_URL: ${{ steps.determine-context.outputs.pr_url }} PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} + TRIGGER_TYPE: ${{ steps.determine-context.outputs.trigger_type }} run: | - echo "Analyzing PR #${PR_NUMBER}" + echo "Analyzing PR #${PR_NUMBER} (trigger: ${TRIGGER_TYPE})" - # Build task prompt - using unquoted heredoc so variables expand - TASK_PROMPT=$(cat <> "${GITHUB_STEP_SUMMARY}" + + - name: Wait for Task Completion + 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() + 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() + 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() + 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}"