diff --git a/.github/workflows/documentation-check.yaml b/.github/workflows/documentation-check.yaml new file mode 100644 index 0000000000000..b95d29d4ab3a3 --- /dev/null +++ b/.github/workflows/documentation-check.yaml @@ -0,0 +1,310 @@ +name: AI Documentation Updates Automation + +on: + pull_request: + types: + - opened + - synchronize + - reopened + workflow_dispatch: + inputs: + pr_number: + description: "Pull Request number to process" + required: true + type: number + +jobs: + documentation-check: + name: Check Documentation Updates with Claude Code + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + CODER_URL: ${{ secrets.TRAIAGE_CODER_URL }} + CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }} + permissions: + contents: read + pull-requests: write + actions: write + + steps: + - name: Determine Inputs + id: determine-inputs + if: always() + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_EVENT_USER_ID: ${{ github.event.sender.id }} + GITHUB_EVENT_USER_LOGIN: ${{ github.event.sender.login }} + INPUTS_PR_NUMBER: ${{ inputs.pr_number }} + GH_TOKEN: ${{ github.token }} + run: | + # For workflow_dispatch, use the actor who triggered it + # For pull_request events, use the PR author + 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}" + 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 number: ${INPUTS_PR_NUMBER}" + echo "pr_number=${INPUTS_PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + exit 0 + elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + GITHUB_USER_ID=${GITHUB_EVENT_USER_ID} + echo "Using PR author: ${GITHUB_EVENT_USER_LOGIN} (ID: ${GITHUB_USER_ID})" + echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}" + echo "github_username=${GITHUB_EVENT_USER_LOGIN}" >> "${GITHUB_OUTPUT}" + + echo "Using PR number: ${GITHUB_EVENT_PR_NUMBER}" + echo "pr_number=${GITHUB_EVENT_PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + exit 0 + else + echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}" + exit 1 + fi + + - name: Verify push access + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + GITHUB_USERNAME: ${{ steps.determine-inputs.outputs.github_username }} + GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }} + run: | + # Query the actor's permission on this repo + can_push="$(gh api "/repos/${GITHUB_REPOSITORY}/collaborators/${GITHUB_USERNAME}/permission" --jq '.user.permissions.push')" + if [[ "${can_push}" != "true" ]]; then + echo "::error title=Access Denied::${GITHUB_USERNAME} does not have push access to ${GITHUB_REPOSITORY}" + exit 1 + fi + + - name: Post initial comment + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.determine-inputs.outputs.pr_number }} + GITHUB_REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + run: | + COMMENT_BODY=$(cat <> "${GITHUB_PATH}" + + - name: Get Coder username from GitHub actor + id: get-coder-username + env: + CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }} + GH_TOKEN: ${{ github.token }} + GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }} + run: | + user_json=$( + coder users list --github-user-id="${GITHUB_USER_ID}" --output=json + ) + coder_username=$(jq -r 'first | .username' <<< "$user_json") + [[ -z "${coder_username}" || "${coder_username}" == "null" ]] && echo "No Coder user with GitHub user ID ${GITHUB_USER_ID} found" && exit 1 + echo "coder_username=${coder_username}" >> "${GITHUB_OUTPUT}" + + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Create Coder task for documentation analysis + id: create-task + env: + CODER_USERNAME: ${{ steps.get-coder-username.outputs.coder_username }} + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ steps.determine-inputs.outputs.pr_number }} + RUN_ID: ${{ github.run_id }} + TEMPLATE_NAME: "traiage" + TEMPLATE_PRESET: "Default" + run: | + # Fetch PR details using `gh` CLI + pr_json=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json 'url') + pr_url=$(echo "${pr_json}" | jq -r '.url') + + # Build comprehensive prompt + PROMPT=$(cat <<'EOF' + Analyze PR ${pr_url} for documentation updates. + + ## Task Description + You are tasked with analyzing code changes in a pull request and determining what documentation updates are needed. Please: + + 1. Use the GitHub CLI or API to fetch the PR details, including: + - PR title and description + - List of files changed + - The actual code diff + 2. Review the PR description and code changes to understand what was changed + 3. Examine the existing documentation in the docs/ directory + 4. Identify any of the following needs: + - Updates required to existing documentation + - Documentation that needs to be deprecated + - New documentation that should be added + 5. Provide a clear, actionable list of documentation changes needed + + ## PR to Analyze + ${pr_url} + + ## IMPORTANT: Final Output Format + After completing your analysis, you MUST end your response with ONLY the following formatted output between the markers: + + ---DOCUMENTATION-ANALYSIS-START--- + [Your concise bulleted list of recommendations OR "No documentation changes needed"] + ---DOCUMENTATION-ANALYSIS-END--- + + The content between these markers should be: + - A simple bulleted list with links to docs that need updating + - OR a single sentence: "No documentation changes needed - [brief reason]" + - Maximum 10-15 lines + - Use markdown formatting + - Include file paths as links when referencing docs + + Example format: + ---DOCUMENTATION-ANALYSIS-START--- + ### Documentation Updates Needed + - **Update** [docs/admin/templates.md](docs/admin/templates.md) - Add new parameter documentation + - **Create** docs/guides/new-feature.md - Document the new feature workflow + - **Deprecate** docs/old-feature.md - Feature has been removed + ---DOCUMENTATION-ANALYSIS-END--- + + OR: + + ---DOCUMENTATION-ANALYSIS-START--- + No documentation changes needed - This PR only contains internal refactoring with no user-facing changes. + ---DOCUMENTATION-ANALYSIS-END--- + EOF + ) + # Expand variables in the prompt + PROMPT=$(eval "cat <> "${GITHUB_OUTPUT}" + echo "TASK_NAME=${CODER_USERNAME}/${TASK_NAME}" >> "${GITHUB_ENV}" + + - name: Update comment with task link + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.determine-inputs.outputs.pr_number }} + GITHUB_REPOSITORY: ${{ github.repository }} + TASK_NAME: ${{ steps.create-task.outputs.TASK_NAME }} + RUN_ID: ${{ github.run_id }} + run: | + COMMENT_BODY=$(cat < /tmp/task_output.txt + echo "${DURATION_STR}" > /tmp/task_duration.txt + + echo "Task completed successfully in ${DURATION_STR}" + + - name: Update PR comment with results + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.determine-inputs.outputs.pr_number }} + GITHUB_REPOSITORY: ${{ github.repository }} + TASK_NAME: ${{ steps.create-task.outputs.TASK_NAME }} + RUN_ID: ${{ github.run_id }} + run: | + TASK_OUTPUT=$(cat /tmp/task_output.txt) + DURATION=$(cat /tmp/task_duration.txt) + + COMMENT_BODY=$(cat <" + echo "Commands:" + echo " create - Create a new documentation check task" + echo " wait - Wait for task to complete" + echo " summary - Get task output summary" + echo " delete - Delete the task" + exit 1 +} + +create() { + requiredenvs CODER_URL CODER_SESSION_TOKEN CODER_USERNAME TASK_NAME TEMPLATE_NAME TEMPLATE_PRESET PROMPT + + # Check if a task already exists + set +e + task_json=$("${CODER_BIN}" \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${CODER_USERNAME}/${TASK_NAME}" \ + --output json 2>/dev/null) + set -e + + if [[ "${TASK_NAME}" == $(jq -r '.name' <<<"${task_json}" 2>/dev/null) ]]; then + echo "Task \"${CODER_USERNAME}/${TASK_NAME}\" already exists. Sending prompt to existing task." + prompt + exit 0 + fi + + "${CODER_BIN}" \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks create \ + --name "${TASK_NAME}" \ + --template "${TEMPLATE_NAME}" \ + --preset "${TEMPLATE_PRESET}" \ + --org coder \ + --owner "${CODER_USERNAME}" \ + --stdin <<<"${PROMPT}" + exit 0 +} + +prompt() { + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME PROMPT + + ${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --watch >/dev/null + + ${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks send "${TASK_NAME}" \ + --stdin \ + <<<"${PROMPT}" +} + +wait_for_completion() { + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME + + ${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --watch >/dev/null +} + +summary() { + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME + + last_msg_json=$( + ${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks logs "${TASK_NAME}" \ + --output json + ) + + # Extract the last output message from the task + last_output_msg=$(jq -r 'last(.[] | select(.type=="output")) | .content' <<<"${last_msg_json}") + + # Extract only the content between the documentation analysis markers + if echo "${last_output_msg}" | grep -q "---DOCUMENTATION-ANALYSIS-START---"; then + # Extract content between markers + summary=$(echo "${last_output_msg}" | sed -n '/---DOCUMENTATION-ANALYSIS-START---/,/---DOCUMENTATION-ANALYSIS-END---/p' | sed '1d;$d') + echo "${summary}" + else + # Fallback: if markers not found, return a message + echo "⚠️ Unable to extract documentation analysis. Please check the [task logs](https://dev.coder.com/tasks/${TASK_NAME})." + fi +} + +delete() { + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME + + "${CODER_BIN}" \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + delete \ + "${TASK_NAME}" \ + --yes + exit 0 +} + +main() { + dependencies coder + + if [[ $# -eq 0 ]]; then + usage + fi + + case "$1" in + create) + create + ;; + wait) + wait_for_completion + ;; + summary) + summary + ;; + delete) + delete + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +} + +main "$@"