---
name: OpenHands PR Review
description: Automated PR review using OpenHands agent
author: OpenHands

branding:
    icon: code
    color: blue

inputs:
    agent-kind:
        description: >
            Review agent backend. Use 'openhands' for the standard OpenHands
            SDK Agent, or 'acp' to run an ACP-compatible agent server.
        required: false
        default: openhands
    llm-model:
        description: >
            LLM model to use for the review. In ACP mode this is passed to the
            ACP server when supported. Can be a comma-separated list for A/B
            testing - one model will be randomly selected per review.
            Example: 'model-a' or 'model-a,model-b,model-c'
        required: false
        default: anthropic/claude-sonnet-4-5-20250929
    acp-command:
        description: >
            Command used to start the ACP server when agent-kind is
            'acp'. The command must be available in the runner environment
            or runnable through a package manager. The value is shell-split
            before execution.
            Examples: 'npx -y @zed-industries/codex-acp@0.12.0',
            'codex-acp', 'claude-agent-acp', or 'npx -y
            @agentclientprotocol/claude-agent-acp'.
        required: false
        default: ''
    acp-prompt-timeout:
        description: Timeout in seconds for one ACP prompt turn.
        required: false
        default: '1800'
    llm-base-url:
        description: LLM base URL (optional, for custom LLM endpoints)
        required: false
        default: ''
    review-style:
        description: "[DEPRECATED] Previously chose between 'standard' and 'roasted' review styles. These have been merged into a single code-review skill. The input is kept for backward compatibility but no longer changes behavior. Will be removed in a future version."
        required: false
        default: roasted
    require-evidence:
        description: "When true, require the reviewer to check the PR description for an Evidence section proving the code works end-to-end (screenshots/videos for frontend changes; commands and runtime output for backend, CLI, or script changes; conversation link when agent-generated). Test output alone does not count."
        required: false
        default: 'false'
    use-sub-agents:
        description: >
            Enable sub-agent delegation for file-level reviews.
            When true, the agent gets the TaskToolSet and decides at runtime
            whether to delegate based on diff size and complexity.
            Disabled by default due to high token costs and potential timeouts
            (see issue #208). Set to 'true' to opt in.
        required: false
        default: 'false'
    load-public-skills:
        description: >
            Load the public skills repository into the review agent context.
            Disable this to keep PR review prompts limited to project skills
            and the pr-review plugin skills.
        required: false
        default: 'true'
    collect-feedback:
        description: >
            Ask maintainers to rate the automated review with thumbs up/down
            reactions by appending a short footer to the main review body.
        required: false
        default: 'true'
    extensions-repo:
        description: GitHub repository for extensions (owner/repo)
        required: false
        default: OpenHands/extensions
    extensions-version:
        description: Git ref to use for extensions (tag, branch, or commit SHA, e.g., v1.0.0, main, or abc1234)
        required: false
        default: main
    openhands-sdk-package:
        description: >
            Package spec passed to uv for openhands-sdk. Keep the default for
            normal use, or override it to pin a specific SDK build for testing
            or rollout control.
        required: false
        default: openhands-sdk
    llm-api-key:
        description: >
            LLM API key. Required when agent-kind is 'openhands'.
            Ignored in ACP mode because the ACP server owns authentication.
        required: false
    github-token:
        description: GitHub token for API access (required)
        required: true
    lmnr-api-key:
        description: Laminar API key for observability (optional)
        required: false
        default: ''
    enable-uv-cache:
        description: >
            Enable setup-uv's GitHub Actions cache for Python dependencies.
            Default is 'false' for security: shared caches can become a pivot
            into more privileged workflows (prompt-injected reviewer could
            write a malicious wheel into cache; subsequent higher-privilege
            workflow hits poisoned cache). Only opt in when you control the
            runner environment (e.g. self-hosted, single-tenant) and accept
            the trade-off.
        required: false
        default: 'false'

runs:
    using: composite
    steps:
        - name: Checkout extensions repository
          uses: actions/checkout@v4
          with:
              repository: ${{ inputs.extensions-repo }}
              ref: ${{ inputs.extensions-version }}
              path: extensions

        - name: Checkout PR repository
          uses: actions/checkout@v4
          with:
              repository: ${{ github.event.pull_request.head.repo.full_name }}
              ref: ${{ github.event.pull_request.head.ref }}
              fetch-depth: 0
              persist-credentials: false
              path: pr-repo
              submodules: recursive

        - name: Set up Python
          uses: actions/setup-python@v5
          with:
              python-version: '3.12'

        # Security: caching is disabled by default. Prompt injection can coerce
        # the reviewer into running commands, and shared caches can become a
        # pivot into more privileged workflows. Opt in via `enable-uv-cache`
        # only when the runner is single-tenant / self-hosted.
        - name: Install uv
          uses: astral-sh/setup-uv@v6
          with:
              enable-cache: ${{ inputs.enable-uv-cache }}

        - name: Install GitHub CLI
          shell: bash
          run: |
              # `gh` is preinstalled on all GitHub-hosted Ubuntu runners, so the
              # apt path only runs on self-hosted runners that lack it. This
              # avoids a ~10s `apt-get update` on every review on hosted runners,
              # and also avoids spurious failures when an apt mirror is flaky.
              if command -v gh >/dev/null 2>&1; then
                  echo "gh already installed: $(gh --version | head -n1)"
              else
                  echo "gh not found, installing via apt..."
                  sudo apt-get update
                  sudo apt-get install -y gh
              fi

        - name: Check required configuration and select model
          id: select-model
          shell: bash
          env:
              AGENT_KIND: ${{ inputs.agent-kind }}
              ACP_COMMAND: ${{ inputs.acp-command }}
              LLM_API_KEY: ${{ inputs.llm-api-key }}
              GITHUB_TOKEN: ${{ inputs.github-token }}
              LLM_MODEL_INPUT: ${{ inputs.llm-model }}
              LLM_BASE_URL: ${{ inputs.llm-base-url }}
              PR_NUMBER: ${{ github.event.pull_request.number }}
              PR_TITLE: ${{ github.event.pull_request.title }}
              REPOSITORY: ${{ github.repository }}
              EXTENSIONS_VERSION: ${{ inputs.extensions-version }}
          run: |
              if [ "$AGENT_KIND" != "openhands" ] && [ "$AGENT_KIND" != "acp" ]; then
                echo "Error: agent-kind must be 'openhands' or 'acp'."
                exit 1
              fi

              if [ "$AGENT_KIND" = "openhands" ] && [ -z "$LLM_API_KEY" ]; then
                echo "Error: llm-api-key is required when agent-kind is 'openhands'."
                exit 1
              fi

              if [ "$AGENT_KIND" = "acp" ] && [ -z "$ACP_COMMAND" ]; then
                echo "Error: acp-command is required when agent-kind is 'acp'."
                exit 1
              fi

              if [ -z "$GITHUB_TOKEN" ]; then
                echo "Error: github-token is required."
                exit 1
              fi

              # Select one model randomly from the comma-separated list
              MODELS_LIST="$LLM_MODEL_INPUT"
              IFS=',' read -ra MODELS <<< "$MODELS_LIST"
              SELECTED_MODEL="${MODELS[$RANDOM % ${#MODELS[@]}]}"
              printf 'selected_model=%s\n' "$SELECTED_MODEL" >> "$GITHUB_OUTPUT"

              printf 'PR Number: %s\n' "$PR_NUMBER"
              printf 'PR Title: %s\n' "$PR_TITLE"
              printf 'Repository: %s\n' "$REPOSITORY"
              printf 'Extensions Version: %s\n' "$EXTENSIONS_VERSION"
              printf 'Agent kind: %s\n' "$AGENT_KIND"
              printf 'Available models: %s\n' "$MODELS_LIST"
              printf 'Selected LLM model: %s\n' "$SELECTED_MODEL"
              if [ "$AGENT_KIND" = "acp" ]; then
                printf 'ACP command: %s\n' "$ACP_COMMAND"
              fi
              if [ -n "$LLM_BASE_URL" ]; then
                printf 'LLM base URL: %s\n' "$LLM_BASE_URL"
              fi

        - name: Preflight review permission smoke test
          shell: bash
          env:
              GITHUB_TOKEN: ${{ inputs.github-token }}
              PR_NUMBER: ${{ github.event.pull_request.number }}
              REPOSITORY: ${{ github.repository }}
          run: |
              if ! REVIEW_ID="$(
                gh api \
                  -X POST \
                  "repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
                  -f body='Preflight review permission smoke test. This pending review will be deleted immediately.' \
                  --jq '.id'
              )"; then
                echo "Failed to create pending review during preflight smoke test"
                echo "The GITHUB_TOKEN lacks 'pull_requests: write' permission."
                echo "Add 'permissions: { pull-requests: write }' to your workflow job."
                exit 1
              fi

              if [ -z "$REVIEW_ID" ]; then
                echo "Preflight smoke test returned an empty review id"
                exit 1
              fi

              if ! gh api \
                -X DELETE \
                "repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}"; then
                echo "Warning: Failed to delete preflight review (ID: ${REVIEW_ID}). This is cosmetic and won't affect the workflow."
              fi

        - name: Run PR review
          shell: bash
          env:
              # Pin uv to the exact Python installed by setup-python so it
              # ignores any .python-version in the PR repo checkout (which
              # could be older *or* newer than 3.12).
              UV_PYTHON: '3.12'
              OPENHANDS_SDK_PACKAGE: ${{ inputs.openhands-sdk-package }}
              AGENT_KIND: ${{ inputs.agent-kind }}
              ACP_COMMAND: ${{ inputs.acp-command }}
              ACP_PROMPT_TIMEOUT: ${{ inputs.acp-prompt-timeout }}
              LLM_MODEL: ${{ steps.select-model.outputs.selected_model }}
              LLM_BASE_URL: ${{ inputs.llm-base-url }}
              REVIEW_STYLE: ${{ inputs.review-style }}
              REQUIRE_EVIDENCE: ${{ inputs.require-evidence }}
              COLLECT_FEEDBACK: ${{ inputs.collect-feedback }}
              REVIEW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
              USE_SUB_AGENTS: ${{ inputs.use-sub-agents }}
              LOAD_PUBLIC_SKILLS: ${{ inputs.load-public-skills }}
              LLM_API_KEY: ${{ inputs.llm-api-key }}
              GITHUB_TOKEN: ${{ inputs.github-token }}
              LMNR_PROJECT_API_KEY: ${{ inputs.lmnr-api-key }}
              PR_NUMBER: ${{ github.event.pull_request.number }}
              PR_TITLE: ${{ github.event.pull_request.title }}
              PR_BODY: ${{ github.event.pull_request.body }}
              PR_BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
              PR_HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
              REPO_NAME: ${{ github.repository }}
          run: |
              cd pr-repo
              uv run --no-project --with "$OPENHANDS_SDK_PACKAGE" --with openhands-tools --with lmnr \
                python ../extensions/plugins/pr-review/scripts/agent_script.py

        - name: Upload logs as artifact
          uses: actions/upload-artifact@v4
          if: always()
          with:
              name: openhands-pr-review-logs
              path: |
                  *.log
                  output/
              retention-days: 7

        - name: Upload Laminar trace info for evaluation
          uses: actions/upload-artifact@v4
          if: success()
          with:
              name: pr-review-trace-${{ github.event.pull_request.number }}
              path: pr-repo/laminar_trace_info.json
              retention-days: 30
              if-no-files-found: ignore
