diff --git a/.github/workflows/pr-describe.yml b/.github/workflows/pr-describe.yml index d3abfcf..ae5b82b 100644 --- a/.github/workflows/pr-describe.yml +++ b/.github/workflows/pr-describe.yml @@ -12,6 +12,8 @@ jobs: # Only run if comment contains /describe and is on a PR if: ${{ (github.event.issue.pull_request && contains(github.event.comment.body, '/describe')) }} runs-on: ubuntu-latest + env: + HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} permissions: contents: read pull-requests: write @@ -20,10 +22,20 @@ jobs: - name: Check out Git repository uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + # Generate GitHub App token so actions appear as the custom app (optional - falls back to github.token) + - name: Get GitHub App token + id: app-token + if: env.HAS_APP_SECRETS == 'true' + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + - name: Validate PR and add reaction id: validate_pr uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = context.issue.number; const commentId = context.payload.comment.id; @@ -54,6 +66,7 @@ jobs: id: pr_details uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const fs = require('fs'); const prNumber = ${{ steps.validate_pr.outputs.pr_number }}; @@ -144,12 +157,14 @@ jobs: **Diff:** $(cat pr.diff) anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + github-token: ${{ steps.app-token.outputs.token || github.token }} timeout: 300000 # 5 minutes - name: Update PR description if: ${{ steps.generate.conclusion == 'success' }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const fs = require('fs'); const prNumber = ${{ steps.validate_pr.outputs.pr_number }}; @@ -172,6 +187,7 @@ jobs: if: ${{ steps.generate.conclusion == 'success' }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = ${{ steps.validate_pr.outputs.pr_number }}; @@ -186,6 +202,7 @@ jobs: if: ${{ failure() && steps.generate.conclusion != 'success' }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = ${{ steps.validate_pr.outputs.pr_number }}; @@ -200,6 +217,7 @@ jobs: if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = ${{ steps.validate_pr.outputs.pr_number }}; const title = '${{ steps.pr_details.outputs.title }}'; diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml new file mode 100644 index 0000000..72bb365 --- /dev/null +++ b/.github/workflows/review-pr.yml @@ -0,0 +1,264 @@ +# Reusable workflow for AI-powered PR reviews +# Usage: +# name: PR Review +# on: +# issue_comment: +# types: [created] +# pull_request_review_comment: +# types: [created] +# pull_request_target: +# types: [ready_for_review, opened] +# +# permissions: +# contents: read +# pull-requests: write +# issues: write +# +# jobs: +# review: +# uses: docker/cagent-action/.github/workflows/review-pr.yml@latest +# secrets: inherit + +name: PR Review + +on: + workflow_call: + inputs: + pr-number: + description: "Pull request number (auto-detected if not provided)" + required: false + type: string + default: "" + comment-id: + description: "Comment ID for reactions (auto-detected if not provided)" + required: false + type: string + default: "" + additional-prompt: + description: "Additional instructions for the review" + required: false + type: string + default: "" + model: + description: "Model to use (e.g., anthropic/claude-sonnet-4-5)" + required: false + type: string + default: "" + cagent-version: + description: "Version of cagent to use" + required: false + type: string + default: "v1.19.7" + auto-review-org: + description: "Organization to check membership for auto-reviews" + required: false + type: string + default: "docker" + secrets: + ORG_MEMBERSHIP_TOKEN: + description: "PAT with read:org scope to check org membership for auto-reviews" + required: false + ANTHROPIC_API_KEY: + description: "Anthropic API key (at least one API key required)" + required: false + OPENAI_API_KEY: + description: "OpenAI API key (at least one API key required)" + required: false + GOOGLE_API_KEY: + description: "Google API key (at least one API key required)" + required: false + AWS_BEARER_TOKEN_BEDROCK: + description: "AWS Bearer token for Bedrock (at least one API key required)" + required: false + XAI_API_KEY: + description: "xAI API key for Grok (at least one API key required)" + required: false + NEBIUS_API_KEY: + description: "Nebius API key (at least one API key required)" + required: false + MISTRAL_API_KEY: + description: "Mistral API key (at least one API key required)" + required: false + CAGENT_REVIEWER_APP_ID: + description: "GitHub App ID for reviewer identity" + required: false + CAGENT_REVIEWER_APP_PRIVATE_KEY: + description: "GitHub App private key" + required: false + outputs: + exit-code: + description: "Exit code from the review" + value: ${{ jobs.auto-review.outputs.exit-code || jobs.manual-review.outputs.exit-code }} + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # ========================================================================== + # AUTOMATIC REVIEW FOR ORG MEMBERS + # Triggers when a PR is marked ready for review or opened (non-draft) + # Only runs for members of the configured org (supports fork-based workflow) + # ========================================================================== + auto-review: + if: | + github.event_name == 'pull_request_target' && + !github.event.pull_request.draft + runs-on: ubuntu-latest + env: + HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} + outputs: + exit-code: ${{ steps.run-review.outputs.exit-code }} + + steps: + - name: Check if PR author is org member + id: membership + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.ORG_MEMBERSHIP_TOKEN }} + script: | + const org = '${{ inputs.auto-review-org }}'; + const username = context.payload.pull_request.user.login; + + try { + await github.rest.orgs.checkMembershipForUser({ + org: org, + username: username + }); + core.setOutput('is_member', 'true'); + console.log(`✅ ${username} is a ${org} org member - proceeding with auto-review`); + } catch (error) { + if (error.status === 404 || error.status === 302) { + core.setOutput('is_member', 'false'); + console.log(`⏭️ ${username} is not a ${org} org member - skipping auto-review`); + } else if (error.status === 401) { + core.setFailed( + '❌ ORG_MEMBERSHIP_TOKEN secret is missing or invalid.\n\n' + + `This secret is required to check ${org} org membership for auto-reviews.\n\n` + + 'To fix this:\n' + + '1. Create a classic PAT with read:org scope at https://github.com/settings/tokens/new\n' + + '2. Add it as an org secret named ORG_MEMBERSHIP_TOKEN' + ); + } else { + core.setFailed(`Failed to check org membership: ${error.message}`); + } + } + + # Safe to checkout PR head because review-pr only READS files (no code execution) + - name: Checkout PR head + if: steps.membership.outputs.is_member == 'true' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + ref: refs/pull/${{ github.event.pull_request.number }}/head + + # Generate GitHub App token for custom app identity (optional - falls back to github.token) + - name: Generate GitHub App token + if: steps.membership.outputs.is_member == 'true' && env.HAS_APP_SECRETS == 'true' + id: app-token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + + - name: Run PR Review + if: steps.membership.outputs.is_member == 'true' + id: run-review + uses: docker/cagent-action/review-pr@latest + with: + pr-number: ${{ inputs.pr-number || github.event.pull_request.number }} + additional-prompt: ${{ inputs.additional-prompt }} + model: ${{ inputs.model }} + cagent-version: ${{ inputs.cagent-version }} + github-token: ${{ steps.app-token.outputs.token || github.token }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + google-api-key: ${{ secrets.GOOGLE_API_KEY }} + aws-bearer-token-bedrock: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} + xai-api-key: ${{ secrets.XAI_API_KEY }} + nebius-api-key: ${{ secrets.NEBIUS_API_KEY }} + mistral-api-key: ${{ secrets.MISTRAL_API_KEY }} + + # ========================================================================== + # MANUAL REVIEW PIPELINE + # Triggers when someone comments /review on a PR + # ========================================================================== + manual-review: + if: github.event.issue.pull_request && contains(github.event.comment.body, '/review') + runs-on: ubuntu-latest + env: + HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} + outputs: + exit-code: ${{ steps.run-review.outputs.exit-code }} + + steps: + # Checkout PR head (not default branch) + # Note: Authorization is handled by the composite action's built-in check + - name: Checkout PR head + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + ref: refs/pull/${{ github.event.issue.number }}/head + + # Generate GitHub App token for custom app identity (optional - falls back to github.token) + - name: Generate GitHub App token + if: env.HAS_APP_SECRETS == 'true' + id: app-token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + + - name: Run PR Review + id: run-review + uses: docker/cagent-action/review-pr@latest + with: + pr-number: ${{ inputs.pr-number || github.event.issue.number }} + comment-id: ${{ inputs.comment-id || github.event.comment.id }} + additional-prompt: ${{ inputs.additional-prompt }} + model: ${{ inputs.model }} + cagent-version: ${{ inputs.cagent-version }} + github-token: ${{ steps.app-token.outputs.token || github.token }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + google-api-key: ${{ secrets.GOOGLE_API_KEY }} + aws-bearer-token-bedrock: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} + xai-api-key: ${{ secrets.XAI_API_KEY }} + nebius-api-key: ${{ secrets.NEBIUS_API_KEY }} + mistral-api-key: ${{ secrets.MISTRAL_API_KEY }} + + # ========================================================================== + # LEARN FROM FEEDBACK + # Processes replies to agent review comments for continuous improvement + # ========================================================================== + learn-from-feedback: + if: github.event_name == 'pull_request_review_comment' && github.event.comment.in_reply_to_id + runs-on: ubuntu-latest + env: + HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Generate GitHub App token for custom app identity (optional - falls back to github.token) + - name: Generate GitHub App token + if: env.HAS_APP_SECRETS == 'true' + id: app-token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + + - name: Learn from user feedback + uses: docker/cagent-action/review-pr/learn@latest + with: + github-token: ${{ steps.app-token.outputs.token || github.token }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + google-api-key: ${{ secrets.GOOGLE_API_KEY }} + aws-bearer-token-bedrock: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} + xai-api-key: ${{ secrets.XAI_API_KEY }} + nebius-api-key: ${{ secrets.NEBIUS_API_KEY }} + mistral-api-key: ${{ secrets.MISTRAL_API_KEY }} diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index d5e7d04..6979c47 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -19,6 +19,8 @@ jobs: security-scan: name: Security Scan with cagent runs-on: ubuntu-latest + env: + HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} permissions: contents: read issues: write @@ -28,6 +30,15 @@ jobs: with: fetch-depth: 0 # Need full history to get commits from past week + # Generate GitHub App token so issues appear as the custom app (optional - falls back to github.token) + - name: Get GitHub App token + id: app-token + if: env.HAS_APP_SECRETS == 'true' + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + - name: Get commits from past week id: commits env: @@ -234,7 +245,7 @@ jobs: - name: Create security issue if: steps.check-issues.outputs.has_issues == 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} OUTPUT_FILE: ${{ steps.scan.outputs.output-file }} COMMIT_COUNT: ${{ steps.commits.outputs.commit_count }} DAYS_BACK: ${{ inputs.days_back || '7' }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65856c8..37e1a62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,6 +105,7 @@ jobs: fi - name: Checkout code + if: steps.fork-check.outputs.is_fork != 'true' uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Run test @@ -163,7 +164,6 @@ jobs: echo "✅ Found agent response content" echo "Response preview: $(echo "$CONTENT" | head -n 1)" - test-invalid-agent: name: Invalid Agent Test runs-on: ubuntu-latest diff --git a/README.md b/README.md index dce442c..71be319 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,13 @@ A GitHub Action for running [cagent](https://github.com/docker/cagent) AI agents with: agent: docker/code-analyzer prompt: "Analyze this code" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` 2. **Configure API key** in your repository settings: - Go to `Settings` → `Secrets and variables` → `Actions` - - Add `ANTHROPIC_API_KEY` with your API key from [Anthropic Console](https://console.anthropic.com/) + - Add `ANTHROPIC_API_KEY` (or another provider's key) from [Anthropic Console](https://console.anthropic.com/) 3. **That's it!** The action will automatically: - Download the cagent binary @@ -46,8 +45,7 @@ See [security/README.md](security/README.md) for complete security documentation with: agent: docker/github-action-security-scanner prompt: "Analyze these commits for security vulnerabilities" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` ### Analyzing Code Changes @@ -81,14 +79,13 @@ jobs: uses: docker/cagent-action@latest with: agent: docker/code-analyzer + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | Analyze these code changes for quality and best practices: ```diff $(cat pr.diff) ``` - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Post analysis run: | @@ -106,8 +103,7 @@ jobs: with: agent: ./agents/my-agent.yaml prompt: "Analyze the codebase" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` ### Advanced Configuration @@ -118,16 +114,16 @@ jobs: with: agent: docker/code-analyzer prompt: "Analyze this codebase" + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} cagent-version: v1.19.7 mcp-gateway: true # Set to true to install mcp-gateway mcp-gateway-version: v0.22.0 yolo: false # Require manual approval timeout: 600 # 10 minute timeout debug: true # Enable debug logging + quiet: false # Show verbose tool calls (default: true) working-directory: ./src extra-args: "--verbose" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` ### Using Outputs @@ -139,8 +135,7 @@ jobs: with: agent: docker/code-analyzer prompt: "Analyze this codebase" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} - name: Check execution time run: | @@ -166,18 +161,23 @@ jobs: | `cagent-version` | Version of cagent to use | No | `v1.19.7` | | `mcp-gateway` | Install mcp-gateway (`true`/`false`) | No | `false` | | `mcp-gateway-version` | Version of mcp-gateway to use (specifying this will enable mcp-gateway installation) | No | `v0.22.0` | -| `anthropic-api-key` | Anthropic API key | No | `$ANTHROPIC_API_KEY` env var | -| `openai-api-key` | OpenAI API key | No | `$OPENAI_API_KEY` env var | -| `google-api-key` | Google API key for Gemini | No | `$GOOGLE_API_KEY` env var | -| `aws-bearer-token-bedrock` | AWS Bearer token for Bedrock models | No | `$AWS_BEARER_TOKEN_BEDROCK` env var | -| `xai-api-key` | xAI API key for Grok models | No | `$XAI_API_KEY` env var | -| `nebius-api-key` | Nebius API key | No | `$NEBIUS_API_KEY` env var | -| `mistral-api-key` | Mistral API key | No | `$MISTRAL_API_KEY` env var | -| `github-token` | GitHub token for API access | No | Auto-provided by GitHub Actions | +| `anthropic-api-key` | Anthropic API key for Claude models (at least one API key required) | No* | - | +| `openai-api-key` | OpenAI API key (at least one API key required) | No* | - | +| `google-api-key` | Google API key for Gemini models (at least one API key required) | No* | - | +| `aws-bearer-token-bedrock` | AWS Bearer token for Bedrock models (at least one API key required) | No* | - | +| `xai-api-key` | xAI API key for Grok models (at least one API key required) | No* | - | +| `nebius-api-key` | Nebius API key (at least one API key required) | No* | - | +| `mistral-api-key` | Mistral API key (at least one API key required) | No* | - | +| `github-token` | GitHub token for API access | No | `github.token` | +| `github-app-id` | GitHub App ID for custom identity (comments/reviews appear as the app) | No | - | +| `github-app-private-key` | GitHub App private key (required if `github-app-id` is provided) | No | - | | `timeout` | Timeout in seconds for agent execution (0 for no timeout) | No | `0` | | `debug` | Enable debug mode with verbose logging (`true`/`false`) | No | `false` | | `working-directory` | Working directory to run the agent in | No | `.` | | `yolo` | Auto-approve all prompts (`true`/`false`) | No | `true` | +| `quiet` | Suppress verbose tool call output (`true`/`false`). Set to `false` for debugging. | No | `true` | +| `max-retries` | Maximum number of retries on failure (0 = no retries) | No | `2` | +| `retry-delay` | Seconds to wait between retries | No | `5` | | `extra-args` | Additional arguments to pass to `cagent exec` | No | - | ## Outputs @@ -192,18 +192,18 @@ jobs: | `secrets-detected` | Whether secrets were detected in output | | `prompt-suspicious` | Whether suspicious patterns were detected in user prompt | -## Environment Variables +## API Keys -The action supports the following environment variables for different AI providers: +**At least one API key is required.** The action validates this at startup and fails fast with a clear error if no API key is provided. -- `ANTHROPIC_API_KEY`: Anthropic API key for Claude models -- `OPENAI_API_KEY`: OpenAI API key for GPT models -- `GOOGLE_API_KEY`: Google API key for Gemini models -- `AWS_BEARER_TOKEN_BEDROCK`: AWS Bearer token for Bedrock models -- `XAI_API_KEY`: xAI API key for Grok models -- `NEBIUS_API_KEY`: Nebius API key -- `MISTRAL_API_KEY`: Mistral API key -- `GITHUB_TOKEN`: Automatically provided by GitHub Actions (for GitHub API access) +Supported providers: +- **Anthropic** (`anthropic-api-key`): Claude models - [Get API key](https://console.anthropic.com/) +- **OpenAI** (`openai-api-key`): GPT models - [Get API key](https://platform.openai.com/) +- **Google** (`google-api-key`): Gemini models - [Get API key](https://aistudio.google.com/) +- **AWS Bedrock** (`aws-bearer-token-bedrock`): Various models via AWS +- **xAI** (`xai-api-key`): Grok models - [Get API key](https://console.x.ai/) +- **Nebius** (`nebius-api-key`): Nebius models +- **Mistral** (`mistral-api-key`): Mistral models - [Get API key](https://console.mistral.ai/) ## Permissions @@ -216,6 +216,7 @@ permissions: issues: write ``` + ## Examples ### Multiple Agents in a Workflow @@ -241,16 +242,14 @@ jobs: with: agent: docker/github-action-security-scanner prompt: "Analyze for security issues" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} - name: Code Quality Analysis uses: docker/cagent-action@latest with: agent: docker/code-quality-analyzer prompt: "Analyze code quality and best practices" - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` ### Manual Trigger with Inputs @@ -279,8 +278,7 @@ jobs: with: agent: ${{ github.event.inputs.agent }} prompt: ${{ github.event.inputs.prompt }} - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` ## Contributing diff --git a/action.yml b/action.yml index 47b10ce..f617465 100644 --- a/action.yml +++ b/action.yml @@ -25,28 +25,34 @@ inputs: required: false default: "v0.22.0" anthropic-api-key: - description: "Anthropic API key (defaults to ANTHROPIC_API_KEY secret)" + description: "Anthropic API key for Claude models (at least one API key required)" required: false openai-api-key: - description: "OpenAI API key (defaults to OPENAI_API_KEY secret)" + description: "OpenAI API key (at least one API key required)" required: false google-api-key: - description: "Google API key for Gemini (defaults to GOOGLE_API_KEY secret)" + description: "Google API key for Gemini models (at least one API key required)" required: false aws-bearer-token-bedrock: - description: "AWS Bearer token for Bedrock models (defaults to AWS_BEARER_TOKEN_BEDROCK secret)" + description: "AWS Bearer token for Bedrock models (at least one API key required)" required: false xai-api-key: - description: "xAI API key for Grok models (defaults to XAI_API_KEY secret)" + description: "xAI API key for Grok models (at least one API key required)" required: false nebius-api-key: - description: "Nebius API key (defaults to NEBIUS_API_KEY secret)" + description: "Nebius API key (at least one API key required)" required: false mistral-api-key: - description: "Mistral API key (defaults to MISTRAL_API_KEY secret)" + description: "Mistral API key (at least one API key required)" required: false github-token: - description: "GitHub token for API access (defaults to GITHUB_TOKEN env var)" + description: "GitHub token for API access (defaults to github.token)" + required: false + github-app-id: + description: "GitHub App ID for custom identity (comments/reviews appear as the app)" + required: false + github-app-private-key: + description: "GitHub App private key (required if github-app-id is provided)" required: false timeout: description: "Timeout in seconds for agent execution (0 for no timeout)" @@ -64,6 +70,10 @@ inputs: description: "Enable yolo mode - auto-approve all prompts (true/false)" required: false default: "true" + quiet: + description: "Suppress verbose tool call output (true/false). Set to false for debugging." + required: false + default: "true" extra-args: description: "Additional arguments to pass to cagent exec" required: false @@ -112,6 +122,14 @@ runs: DEBUG: ${{ inputs.debug }} YOLO: ${{ inputs.yolo }} EXTRA_ARGS: ${{ inputs.extra-args }} + # API keys (explicit inputs only - no env var fallback) + ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }} + OPENAI_API_KEY: ${{ inputs.openai-api-key }} + GOOGLE_API_KEY: ${{ inputs.google-api-key }} + AWS_BEARER_TOKEN_BEDROCK: ${{ inputs.aws-bearer-token-bedrock }} + XAI_API_KEY: ${{ inputs.xai-api-key }} + NEBIUS_API_KEY: ${{ inputs.nebius-api-key }} + MISTRAL_API_KEY: ${{ inputs.mistral-api-key }} run: | # Validate agent is provided if [[ -z "$AGENT" ]]; then @@ -133,6 +151,14 @@ runs: fi fi + # Validate at least one API key is provided (explicit input required) + if [[ -z "$ANTHROPIC_API_KEY" && -z "$OPENAI_API_KEY" && -z "$GOOGLE_API_KEY" && \ + -z "$AWS_BEARER_TOKEN_BEDROCK" && -z "$XAI_API_KEY" && -z "$NEBIUS_API_KEY" && \ + -z "$MISTRAL_API_KEY" ]]; then + echo "::error::At least one API key is required. Provide one of: anthropic-api-key, openai-api-key, google-api-key, aws-bearer-token-bedrock, xai-api-key, nebius-api-key, or mistral-api-key" + exit 1 + fi + if [[ "$DEBUG" == "true" ]]; then echo "::debug::Validation passed" echo "::debug::agent: $AGENT" @@ -171,6 +197,38 @@ runs: # Run the authorization check $ACTION_PATH/security/check-auth.sh "$COMMENT_ASSOCIATION" "$ALLOWED_ROLES" + # ======================================== + # GitHub App Token (Optional) + # ======================================== + + # Generate token if GitHub App credentials are provided + - name: Generate GitHub App token + id: app-token + if: inputs.github-app-id != '' + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ inputs.github-app-id }} + private_key: ${{ inputs.github-app-private-key }} + + - name: Resolve GitHub token + id: resolve-token + shell: bash + run: | + if [ -n "$APP_TOKEN" ]; then + echo "✅ Using GitHub App token" + echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + elif [ -n "$EXPLICIT_TOKEN" ]; then + echo "✅ Using provided github-token" + echo "token=$EXPLICIT_TOKEN" >> $GITHUB_OUTPUT + else + echo "ℹ️ Using default github.token" + echo "token=$DEFAULT_TOKEN" >> $GITHUB_OUTPUT + fi + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + EXPLICIT_TOKEN: ${{ inputs.github-token }} + DEFAULT_TOKEN: ${{ github.token }} + # ======================================== # SECURITY: Sanitize and Analyze Input # ======================================== @@ -344,14 +402,14 @@ runs: id: run-agent shell: bash env: - ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key || env.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ inputs.openai-api-key || env.OPENAI_API_KEY }} - GOOGLE_API_KEY: ${{ inputs.google-api-key || env.GOOGLE_API_KEY }} - AWS_BEARER_TOKEN_BEDROCK: ${{ inputs.aws-bearer-token-bedrock || env.AWS_BEARER_TOKEN_BEDROCK }} - XAI_API_KEY: ${{ inputs.xai-api-key || env.XAI_API_KEY }} - NEBIUS_API_KEY: ${{ inputs.nebius-api-key || env.NEBIUS_API_KEY }} - MISTRAL_API_KEY: ${{ inputs.mistral-api-key || env.MISTRAL_API_KEY }} - GH_TOKEN: ${{ inputs.github-token || github.token }} + ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }} + OPENAI_API_KEY: ${{ inputs.openai-api-key }} + GOOGLE_API_KEY: ${{ inputs.google-api-key }} + AWS_BEARER_TOKEN_BEDROCK: ${{ inputs.aws-bearer-token-bedrock }} + XAI_API_KEY: ${{ inputs.xai-api-key }} + NEBIUS_API_KEY: ${{ inputs.nebius-api-key }} + MISTRAL_API_KEY: ${{ inputs.mistral-api-key }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} AGENT: ${{ inputs.agent }} PROMPT_INPUT: ${{ inputs.prompt }} ACTION_PATH: ${{ github.action_path }} @@ -362,9 +420,20 @@ runs: WORKING_DIR: ${{ inputs.working-directory }} CAGENT_VERSION: ${{ inputs.cagent-version }} MCP_INSTALLED: ${{ steps.setup-binaries.outputs.mcp-installed }} + QUIET: ${{ inputs.quiet }} run: | set -e + # Mask all API keys to prevent accidental exposure in logs + [ -n "$ANTHROPIC_API_KEY" ] && echo "::add-mask::$ANTHROPIC_API_KEY" + [ -n "$OPENAI_API_KEY" ] && echo "::add-mask::$OPENAI_API_KEY" + [ -n "$GOOGLE_API_KEY" ] && echo "::add-mask::$GOOGLE_API_KEY" + [ -n "$AWS_BEARER_TOKEN_BEDROCK" ] && echo "::add-mask::$AWS_BEARER_TOKEN_BEDROCK" + [ -n "$XAI_API_KEY" ] && echo "::add-mask::$XAI_API_KEY" + [ -n "$NEBIUS_API_KEY" ] && echo "::add-mask::$NEBIUS_API_KEY" + [ -n "$MISTRAL_API_KEY" ] && echo "::add-mask::$MISTRAL_API_KEY" + [ -n "$GH_TOKEN" ] && echo "::add-mask::$GH_TOKEN" + # Change to working directory cd "$WORKING_DIR" @@ -388,6 +457,11 @@ runs: ARGS+=("--yolo") fi + # Quiet mode: suppress verbose tool output (default: true) + if [ "$QUIET" = "true" ]; then + ARGS+=("--hide-tool-calls" "--hide-tool-results") + fi + # Add extra args if provided # Note: This uses simple word splitting. Quoted arguments with spaces are not supported. # Using eval would be a security risk with user-provided input. @@ -578,7 +652,7 @@ runs: if: steps.sanitize-output.outputs.leaked == 'true' shell: bash env: - GH_TOKEN: ${{ inputs.github-token || github.token }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} REPOSITORY: ${{ github.repository }} RUN_ID: ${{ github.run_id }} run: | diff --git a/review-pr/README.md b/review-pr/README.md index 7476741..89b82a1 100644 --- a/review-pr/README.md +++ b/review-pr/README.md @@ -11,6 +11,82 @@ Add `.github/workflows/pr-review.yml` to your repo: ```yaml name: PR Review +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + pull_request_target: + types: [ready_for_review, opened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + review: + uses: docker/cagent-action/.github/workflows/review-pr.yml@latest + secrets: inherit +``` + +### Customizing for your organization + +```yaml +jobs: + review: + uses: docker/cagent-action/.github/workflows/review-pr.yml@latest + with: + auto-review-org: my-org # Only auto-review PRs from this org's members + model: anthropic/claude-haiku-4 # Use a faster/cheaper model + secrets: inherit +``` + +### 2. That's it! + +The workflow automatically handles: + +| Trigger | Behavior | +|---------|----------| +| PR opened/ready | Auto-reviews PRs from Docker org members | +| `/review` comment | Manual review on any PR | +| Reply to review comment | Learns from feedback to improve future reviews | + +--- + +## Required Secrets + +### Minimal Setup (Just API Key) + +| Secret | Description | +|--------|-------------| +| `ANTHROPIC_API_KEY` | Anthropic API key for Claude models* | + +*Or another supported provider's API key (OpenAI, Google, etc.) + +With just an API key, you can use `/review` comments to trigger reviews manually. + +### Full Setup (Auto-Review + Custom Identity) + +| Secret | Description | Purpose | +|--------|-------------|---------| +| `ANTHROPIC_API_KEY` | API key for your LLM provider | Required | +| `ORG_MEMBERSHIP_TOKEN` | PAT with `read:org` scope | Auto-review PRs from org members | +| `CAGENT_REVIEWER_APP_ID` | GitHub App ID | Reviews appear as your app (not github-actions[bot]) | +| `CAGENT_REVIEWER_APP_PRIVATE_KEY` | GitHub App private key | Required with App ID | + +**Note:** Without `ORG_MEMBERSHIP_TOKEN`, only `/review` comments work (no auto-review on PR open). +Without GitHub App secrets, reviews appear as "github-actions[bot]" which is fine for most teams. + +--- + +## Advanced: Using the Composite Action Directly + +For more control over the workflow, use the composite action instead of the reusable workflow: + +```yaml +name: PR Review + on: issue_comment: types: [created] @@ -28,14 +104,15 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # Full history needed for accurate diffs + fetch-depth: 0 + ref: refs/pull/${{ github.event.issue.number }}/head - uses: docker/cagent-action/review-pr@latest with: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + github-token: ${{ secrets.GITHUB_TOKEN }} learn: - # Triggers when someone REPLIES to a review comment (for learning from feedback) if: github.event_name == 'pull_request_review_comment' && github.event.comment.in_reply_to_id runs-on: ubuntu-latest steps: @@ -46,17 +123,6 @@ jobs: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} ``` -### 2. Add your API key - -**Settings** → **Secrets and variables** → **Actions** → Add `ANTHROPIC_API_KEY` - -> **Note:** You only need ONE API key. The examples use Anthropic, but you can use any supported provider (OpenAI, Google, xAI, etc.). - -### 3. Use it - -- Comment `/review` on any PR to trigger a review -- **Reply directly** to review comments to teach the agent (the learning system detects replies to its own comments) - --- ## Adding Language-Specific Guidelines @@ -142,7 +208,20 @@ Override for more thorough or cost-effective reviews: ## Inputs -### `review-pr` +### Reusable Workflow + +When using `docker/cagent-action/.github/workflows/review-pr.yml`: + +| Input | Description | Default | +|-------|-------------|---------| +| `pr-number` | PR number (auto-detected from event) | - | +| `comment-id` | Comment ID for reactions (auto-detected) | - | +| `additional-prompt` | Additional review guidelines | - | +| `model` | Model override (e.g., `anthropic/claude-haiku-4`) | - | +| `cagent-version` | CAgent version | `v1.19.7` | +| `auto-review-org` | Organization for auto-review membership check | `docker` | + +### `review-pr` (Composite Action) PR number and comment ID are auto-detected from `github.event` when not provided. @@ -162,6 +241,8 @@ PR number and comment ID are auto-detected from `github.event` when not provided | `nebius-api-key` | Nebius API key | No* | | `mistral-api-key` | Mistral API key | No* | | `github-token` | GitHub token | No | +| `github-app-id` | GitHub App ID for custom identity | No | +| `github-app-private-key` | GitHub App private key | No | | `cagent-version` | CAgent version | No | *At least one API key is required. @@ -172,17 +253,19 @@ Comment data is read automatically from `github.event.comment`. | Input | Description | Required | |-------|-------------|----------| -| `anthropic-api-key` | Anthropic API key | No | -| `openai-api-key` | OpenAI API key | No | -| `google-api-key` | Google API key (Gemini) | No | -| `aws-bearer-token-bedrock` | AWS Bedrock token | No | -| `xai-api-key` | xAI API key (Grok) | No | -| `nebius-api-key` | Nebius API key | No | -| `mistral-api-key` | Mistral API key | No | +| `anthropic-api-key` | Anthropic API key | No* | +| `openai-api-key` | OpenAI API key | No* | +| `google-api-key` | Google API key (Gemini) | No* | +| `aws-bearer-token-bedrock` | AWS Bedrock token | No* | +| `xai-api-key` | xAI API key (Grok) | No* | +| `nebius-api-key` | Nebius API key | No* | +| `mistral-api-key` | Mistral API key | No* | | `github-token` | GitHub token | No | | `model` | Model override | No | | `cagent-version` | CAgent version | No | +*At least one API key is required. + --- ## Cost @@ -248,11 +331,13 @@ PR Diff → Drafter (hypotheses) → Verifier (confirm) → Post Comments ### Learning System When you reply to a review comment: -1. Action checks if it's a reply to an agent comment +1. Action checks if it's a reply to an agent comment (via `` marker) 2. If yes, processes your feedback 3. Stores learnings in a memory database (cached per-repo) 4. Future reviews avoid the same mistakes +**Memory persistence:** The memory database is stored in GitHub Actions cache. Each run restores the previous cache, adds new learnings, and saves with a unique key. Old caches are automatically cleaned up (keeping the 5 most recent) to prevent cache proliferation while supporting concurrent reviews. + --- ## What It Reviews diff --git a/review-pr/action.yml b/review-pr/action.yml index f49b2bf..bb17548 100644 --- a/review-pr/action.yml +++ b/review-pr/action.yml @@ -37,7 +37,13 @@ inputs: description: "Mistral API key" required: false github-token: - description: "GitHub token for API access" + description: "GitHub token for API access (overridden if github-app-id is provided)" + required: false + github-app-id: + description: "GitHub App ID for custom app identity (comments/reviews will appear as the app)" + required: false + github-app-private-key: + description: "GitHub App private key (required if github-app-id is provided)" required: false cagent-version: description: "Version of cagent to use" @@ -96,11 +102,41 @@ runs: echo "ℹ️ No comment ID - reactions will be skipped" fi + # Generate GitHub App token if credentials are provided + # This allows comments/reviews to appear as the custom app instead of github-actions[bot] + - name: Generate GitHub App token + id: app-token + if: inputs.github-app-id != '' + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ inputs.github-app-id }} + private_key: ${{ inputs.github-app-private-key }} + + # Resolve which token to use: app token > explicit github-token > default github.token + - name: Resolve GitHub token + id: resolve-token + shell: bash + run: | + if [ -n "$APP_TOKEN" ]; then + echo "✅ Using GitHub App token" + echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + elif [ -n "$EXPLICIT_TOKEN" ]; then + echo "✅ Using provided github-token" + echo "token=$EXPLICIT_TOKEN" >> $GITHUB_OUTPUT + else + echo "ℹ️ Using default github.token" + echo "token=$DEFAULT_TOKEN" >> $GITHUB_OUTPUT + fi + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + EXPLICIT_TOKEN: ${{ inputs.github-token }} + DEFAULT_TOKEN: ${{ github.token }} + - name: Add eyes reaction if: steps.resolve-context.outputs.comment-id != '' shell: bash env: - GH_TOKEN: ${{ inputs.github-token || github.token }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} run: | gh api repos/${{ github.repository }}/issues/comments/${{ steps.resolve-context.outputs.comment-id }}/reactions \ -X POST -f content='eyes' || true @@ -109,7 +145,7 @@ runs: id: pr-info shell: bash env: - GH_TOKEN: ${{ inputs.github-token || github.token }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} PR_NUMBER: ${{ steps.resolve-context.outputs.pr-number }} run: | gh pr view $PR_NUMBER --json files -q '.files[].path' > changed_files.txt @@ -204,7 +240,7 @@ runs: xai-api-key: ${{ inputs.xai-api-key }} nebius-api-key: ${{ inputs.nebius-api-key }} mistral-api-key: ${{ inputs.mistral-api-key }} - github-token: ${{ inputs.github-token }} + github-token: ${{ steps.resolve-token.outputs.token }} cagent-version: ${{ inputs.cagent-version }} extra-args: ${{ inputs.model && format('--model={0}', inputs.model) || '' }} @@ -219,7 +255,7 @@ runs: if: always() shell: bash env: - GH_TOKEN: ${{ inputs.github-token || github.token }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} run: | # Keep the 5 most recent caches, delete older ones # This prevents proliferation while handling concurrent runs safely @@ -282,7 +318,7 @@ runs: if: steps.resolve-context.outputs.comment-id != '' && always() shell: bash env: - GH_TOKEN: ${{ inputs.github-token || github.token }} + GH_TOKEN: ${{ steps.resolve-token.outputs.token }} EXIT_CODE: ${{ steps.run-review.outputs.exit-code }} PR_NUMBER: ${{ steps.resolve-context.outputs.pr-number }} run: |