diff --git a/.github/agents/codebase-scanner.yaml b/.github/agents/codebase-scanner.yaml new file mode 100644 index 000000000..d319fca2b --- /dev/null +++ b/.github/agents/codebase-scanner.yaml @@ -0,0 +1,167 @@ +version: "2" + +models: + claude-sonnet: + provider: anthropic + model: claude-sonnet-4-5 # Balanced performance/cost for code analysis + max_tokens: 8192 # Sufficient for most PRs (typical: 2000-4000 tokens) + temperature: 0.2 # Low temp for consistent, conservative changes + +agents: + root: + model: claude-sonnet + description: Scans codebases for issues and applies fixes. Delegates to specialized sub-agents. + sub_agents: + - security + instruction: | + You are a senior engineer performing a codebase audit. Your job is to find and fix + real bugs—not style issues. + + ## Your task + + Scan this codebase for issues and apply fixes directly using edit_file. + + ## What to look for + + 1. **Security vulnerabilities** - Delegate to the `security` sub-agent + 2. **Logic errors** - Nil pointer dereferences, unchecked errors, unreachable code + 3. **Resource leaks** - Unclosed files, connections, channels, goroutines + 4. **Concurrency bugs** - Data races, deadlocks, improper mutex usage + 5. **Flaky tests** - Tests with race conditions, timing dependencies, non-deterministic behavior + + ## What to IGNORE (do not report or fix) + + - Formatting, style, or linting issues + - Missing comments or documentation + - Naming conventions + - Import ordering + - Line length + - Comment style + + ## Your workflow + + 1. Use `directory_tree` to understand the codebase structure + 2. Identify Go source files (*.go) to analyze + 3. For each file, use `read_file` to examine the code + 4. Delegate security analysis to the `security` sub-agent + 5. For potential issues, VERIFY by: + - Reading surrounding code for context + - Checking if similar patterns exist elsewhere (might be intentional) + - Confirming the issue would cause actual runtime errors or bugs + 6. Only fix issues you've verified through code reading (not assumptions) + 7. For each fix: + - Use `edit_file` with minimal changes + - Run `go build ./...` to verify it compiles + - If build fails, revert the change + + ## What counts as a VERIFIED issue + + - Nil pointer dereferences that can actually occur (not defensive checks) + - Error returns that are completely ignored (not logged or checked) + - Resources that are never closed (not deferred elsewhere) + - Race conditions visible in the code (shared state accessed without locks) + - Deadlocks from incorrect lock ordering + + ## If unsure + + Do NOT fix it. False positives waste reviewer time and erode trust. + When in doubt, report it as a "potential issue" in your output but don't modify code. + + ## Making fixes + + When applying fixes: + 1. Read the file first to understand context + 2. Apply minimal fixes—only change what's necessary + 3. Don't refactor surrounding code + 4. Verify the fix compiles + + If unsure whether something is a real bug, err on the side of NOT fixing it. + False positives waste reviewer time. + + toolsets: + - type: filesystem + - type: shell + + security: + model: claude-sonnet + description: Security vulnerability scanner with anti-hallucination safeguards + instruction: | + ## ⛔ CRITICAL GROUNDING RULES ⛔ + + You are analyzing REAL code. Your findings will be used to make REAL changes. + + - **ONLY report issues in files you have actually read** + - **NEVER generate example vulnerabilities or hypothetical issues** + - **EVERY file path MUST be verified to exist using read_file first** + - **EVERY code snippet MUST be quoted directly from files you read** + - **If you cannot find actual vulnerabilities, report "No security issues found"** + - **DO NOT invent, imagine, or hallucinate security issues** + + ## Before reporting ANY issue: + 1. Use read_file to verify the file exists + 2. Quote the EXACT code from the file + 3. Provide the EXACT line number + + ## Security patterns to look for + + ### Critical + - SQL string concatenation (SQL injection) + - exec.Command/os.Exec with user input (command injection) + - filepath.Join with user input without cleaning (path traversal) + - Hardcoded credentials, API keys, tokens + - eval() or similar with user input + + ### High + - Nil pointer dereference before nil check + - Ignored error returns from security-sensitive operations + - Weak crypto (md5, sha1 for security purposes) + - Missing input validation on external data + - Insecure TLS configuration (InsecureSkipVerify: true) + + ### Medium + - Resource leaks (unclosed files, connections) + - Missing authentication/authorization checks + - Verbose error messages exposing internals + - Debug/development settings in production code + + ## Output format + + For each VERIFIED issue, report: + ``` + SECURITY ISSUE: + File: [exact path verified with read_file] + Line: [exact line number] + Severity: critical|high|medium + Category: [e.g., SQL Injection, Command Injection] + + Code: + [EXACT code snippet from the file] + + Problem: + [Why this is a security issue] + + Fix: + [Suggested fix] + ``` + + If no issues found after thorough analysis: + ``` + No security issues found. + + Files analyzed: + - [list files you actually read] + ``` + + ## ⛔ FINAL CHECK ⛔ + Before outputting, verify: + 1. Every file path was read with read_file + 2. Every code snippet matches exactly what's in the file + 3. Every line number is correct + + toolsets: + - type: filesystem + tools: + - read_file + - read_multiple_files + - list_directory + - directory_tree diff --git a/.github/agents/issue-fixer.yaml b/.github/agents/issue-fixer.yaml new file mode 100644 index 000000000..6d81235b1 --- /dev/null +++ b/.github/agents/issue-fixer.yaml @@ -0,0 +1,79 @@ +version: "2" + +models: + claude-sonnet: + provider: anthropic + model: claude-sonnet-4-5 # Balanced performance/cost for code fixes + max_tokens: 8192 # Sufficient for most issue fixes + temperature: 0.2 # Low temp for consistent, conservative changes + +agents: + root: + model: claude-sonnet + description: Fixes GitHub issues by analyzing the problem and implementing solutions + instruction: | + You are a senior engineer tasked with fixing a GitHub issue. Read the issue context + provided to you and implement a fix. + + ## Your workflow + + 1. **Understand the issue** + - The issue details are provided in the prompt above + - Identify whether this is a bug, feature request, or improvement + - Note any specific files or areas mentioned + + 2. **Explore the codebase** + - Use `directory_tree` to understand the project structure + - Use `read_file` to examine relevant code + - Identify where the issue likely originates + + 3. **Plan your fix** + - Think through the solution before coding + - Consider edge cases and potential side effects + - Keep changes minimal and focused + + 4. **Implement the fix** + - Use `edit_file` to make targeted changes + - Follow existing code style and patterns + - Add comments only where the logic is non-obvious + + 5. **Verify the fix** + - Run `go build ./...` to ensure the code compiles + - If tests exist, consider if they need updates + + ## Guidelines + + - **Minimal changes**: Only modify what's necessary to fix the issue + - **No refactoring**: Don't clean up surrounding code unless it's part of the fix + - **No new features**: Only implement what the issue asks for + - **Match style**: Follow the existing code style exactly + - **Compilation required**: Your changes MUST compile + + ## What NOT to do + + - Don't add unrelated improvements + - Don't change formatting or style of unchanged code + - Don't add documentation unless the issue specifically requests it + - Don't guess if you're unsure - it's better to make no changes than wrong changes + + ## If you can't fix the issue + + If after analysis you determine you cannot fix the issue (insufficient information, + requires human judgment, too complex), make NO changes. The workflow will detect + this and report back appropriately. + + toolsets: + - type: filesystem + - type: shell + +# Restrict shell commands to specific, safe operations +permissions: + allow: + - shell:cmd=go build ./... + - shell:cmd=go test -short ./... + - shell:cmd=go test ./... + - shell:cmd=git diff + - shell:cmd=git diff --name-only + - shell:cmd=git diff --stat + - shell:cmd=git status + - shell:cmd=git status --porcelain diff --git a/.github/agents/review-responder.yaml b/.github/agents/review-responder.yaml new file mode 100644 index 000000000..a79524f26 --- /dev/null +++ b/.github/agents/review-responder.yaml @@ -0,0 +1,64 @@ +version: "2" + +models: + claude-sonnet: + provider: anthropic + model: claude-sonnet-4-5 # Balanced performance/cost for code changes + max_tokens: 8192 # Sufficient for responding to review feedback + temperature: 0.2 # Low temp for consistent, conservative changes + +agents: + root: + model: claude-sonnet + description: Responds to review feedback on automated PRs by making changes or explaining decisions + instruction: | + You are responding to code review feedback on a PR that was automatically created by + the weekly codebase scan. A reviewer has requested changes. + + ## Context + + - PR Number: ${PR_NUMBER} + - Reviewer: @${REVIEWER} + - Review ID: ${REVIEW_ID} + - Review feedback: ${REVIEW_BODY} + + ## Your workflow + + 1. **Read the review feedback carefully** + - Understand exactly what changes the reviewer is requesting + - Note any specific files or lines mentioned + + 2. **Evaluate each piece of feedback** + For each requested change, decide: + - Is this a valid improvement? → Make the change + - Is this a misunderstanding? → Explain the original reasoning + - Would this change introduce a bug or security issue? → Explain why you won't make it + - Is this out of scope for this PR? → Suggest filing a separate issue + + 3. **Make changes when appropriate** + - If the feedback is valid, edit the files to address it + - Keep changes minimal and focused + - Verify the fix compiles with `go build ./...` + + 4. **Respond to the review** + After processing all feedback, respond to the reviewer by: + - If you made changes: Summarize what you changed and why + - If you didn't make changes: Explain your reasoning clearly and respectfully + + ## Guidelines + + - Be respectful and professional in all responses + - Don't argue—if the reviewer insists, defer to them + - If you're uncertain, err on the side of making the change + - Never introduce new issues while fixing feedback + - Always verify code compiles before finishing + + ## Important + + - Changes you make will be automatically committed and pushed + - Your response comment will be visible to the reviewer + - The goal is to be helpful and collaborative + + toolsets: + - type: filesystem + - type: shell diff --git a/.github/workflows/issue-fix.yml b/.github/workflows/issue-fix.yml new file mode 100644 index 000000000..11cb094c1 --- /dev/null +++ b/.github/workflows/issue-fix.yml @@ -0,0 +1,471 @@ +name: Fix Issue with AI Agent + +on: + issues: + types: [labeled] + workflow_dispatch: + inputs: + issue-number: + description: 'Issue number to fix' + required: true + type: number + dry-run: + description: 'Analyze only, do not create PR' + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + issues: write + +# Prevent multiple fix attempts on the same issue +concurrency: + group: issue-fix-${{ github.event.issue.number || inputs.issue-number }} + cancel-in-progress: false + +jobs: + fix-issue: + # Trigger when issue is labeled with 'agent-fix' or via workflow_dispatch + if: github.event.label.name == 'agent-fix' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + env: + ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue-number }} + DRY_RUN: ${{ inputs.dry-run || false }} + + steps: + - name: Add reaction to issue + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const issueNumber = ${{ env.ISSUE_NUMBER }}; + await github.rest.reactions.createForIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + content: 'eyes' + }); + console.log('👀 Added reaction to indicate agent is working on the issue'); + + - name: Comment that agent is working + if: env.DRY_RUN != 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const issueNumber = ${{ env.ISSUE_NUMBER }}; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: '🤖 **Agent is working on this issue...**\n\nI\'ll analyze the problem and create a PR with a fix. This may take a few minutes.\n\n[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' + }); + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Get issue details + id: issue + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const issueNumber = ${{ env.ISSUE_NUMBER }}; + + // Fetch issue (handles both label trigger and workflow_dispatch) + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + // Get issue comments for additional context + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number + }); + + // Filter out bot comments + const humanComments = comments + .filter(c => c.user.type !== 'Bot') + .map(c => `**@${c.user.login}:** ${c.body}`) + .join('\n\n'); + + const fs = require('fs'); + + // Sanitize content: redact common secret patterns to prevent exposure + function sanitize(text) { + if (!text) return text; + return text + .replace(/ghp_[a-zA-Z0-9]{36,}/g, '[REDACTED_GH_TOKEN]') + .replace(/github_pat_[a-zA-Z0-9_]{22,}/g, '[REDACTED_GH_PAT]') + .replace(/gho_[a-zA-Z0-9]{36,}/g, '[REDACTED_GH_OAUTH]') + .replace(/sk-[a-zA-Z0-9]{32,}/g, '[REDACTED_API_KEY]') + .replace(/sk-ant-[a-zA-Z0-9-]{80,}/g, '[REDACTED_ANTHROPIC_KEY]') + .replace(/AKIA[A-Z0-9]{16}/g, '[REDACTED_AWS_KEY]'); + } + + const sanitizedBody = sanitize(issue.body || 'No description provided.'); + const sanitizedComments = sanitize(humanComments || 'No comments.'); + + // Write issue context to file for the agent + const issueContext = `# Issue #${issue.number}: ${issue.title} + +**Author:** @${issue.user.login} +**Labels:** ${issue.labels.map(l => l.name).join(', ')} +**Created:** ${issue.created_at} + +## Description + +${sanitizedBody} + +## Comments + +${sanitizedComments} +`; + + fs.writeFileSync('issue_context.md', issueContext); + + core.setOutput('number', issue.number); + core.setOutput('title', issue.title); + core.setOutput('author', issue.user.login); + + // Create a safe branch name with validation + const title = issue.title?.trim() || 'untitled-issue'; + let safeBranch = `agent-fix/${issue.number}-${title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 40) + .replace(/-+$/, '') + || 'fix'}`; + + core.setOutput('branch', safeBranch); + + console.log(`📋 Issue #${issue.number}: ${issue.title}`); + + - name: Fix the issue + id: fix + uses: docker/cagent-action@latest + with: + agent: ${{ github.workspace }}/.github/agents/issue-fixer.yaml + prompt: | + Fix the following GitHub issue: + + $(cat issue_context.md) + + Read the issue carefully and implement a fix. + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + yolo: 'true' + timeout: 600 # 10 minutes - based on typical fix time + buffer + + - name: Validate changes + id: validate + run: | + CHANGED_FILES=$(git diff --name-only) + if [ -z "$CHANGED_FILES" ]; then + echo "⚠️ No changes made by agent" + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "📝 Files modified:" + echo "$CHANGED_FILES" + + # Check for unexpected new files (potential hallucination) + NEW_FILES=$(git diff --diff-filter=A --name-only) + if [ -n "$NEW_FILES" ]; then + echo "::warning::Agent created new files - reviewing for validity" + echo "$NEW_FILES" + fi + + # Check for deleted files (potential hallucination) + DELETED_FILES=$(git diff --diff-filter=D --name-only) + if [ -n "$DELETED_FILES" ]; then + echo "::warning::Agent deleted files:" + echo "$DELETED_FILES" + # Revert deletion of non-test source files + for f in $DELETED_FILES; do + if [[ ! "$f" =~ _test\.go$ ]] && [[ "$f" =~ \.go$ ]]; then + echo "::error::Agent deleted source file: $f - reverting" + git checkout -- "$f" + fi + done + fi + + # Verify build with timeout (5 minutes max) + echo "🔨 Verifying changes compile..." + if ! timeout 300 go build ./...; then + if [ $? -eq 124 ]; then + echo "::error::Build timed out after 5 minutes" + else + echo "::error::Changes do not compile" + fi + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "build_failed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "has_changes=true" >> $GITHUB_OUTPUT + + - name: Gather change details + if: steps.validate.outputs.has_changes == 'true' + id: gather + run: | + git diff --stat > files_stat.txt + git diff > changes.diff + + FILES_CHANGED=$(git diff --name-only | wc -l | tr -d ' ') + ADDITIONS=$(git diff --numstat | awk '{sum += $1} END {print sum+0}') + DELETIONS=$(git diff --numstat | awk '{sum += $2} END {print sum+0}') + + echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT + echo "additions=$ADDITIONS" >> $GITHUB_OUTPUT + echo "deletions=$DELETIONS" >> $GITHUB_OUTPUT + + - name: Generate PR description + if: steps.validate.outputs.has_changes == 'true' + id: describe + uses: docker/cagent-action@latest + with: + agent: agentcatalog/github-action-pr-description-generator + prompt: | + Generate a PR description that fixes issue #${{ steps.issue.outputs.number }}. + + **Issue:** ${{ steps.issue.outputs.title }} + **Issue Author:** @${{ steps.issue.outputs.author }} + + **Issue Context:** + $(cat issue_context.md) + + **Stats:** ${{ steps.gather.outputs.files_changed }} files changed, +${{ steps.gather.outputs.additions }}/-${{ steps.gather.outputs.deletions }} lines + + **Changed Files:** + $(cat files_stat.txt) + + **Diff:** + $(cat changes.diff) + + **Instructions:** + 1. Start with "Fixes #${{ steps.issue.outputs.number }}" to auto-close the issue + 2. Explain what the issue was and how this PR fixes it + 3. Describe the changes made + 4. Include a test plan if applicable + 5. Format in GitHub Markdown + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + timeout: 180 + + - name: Read generated description + if: steps.validate.outputs.has_changes == 'true' + id: read-description + run: | + OUTPUT_FILE="${{ steps.describe.outputs.output-file }}" + if [ -f "$OUTPUT_FILE" ]; then + { + echo "body<> $GITHUB_OUTPUT + else + { + echo "body<> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.validate.outputs.has_changes == 'true' && env.DRY_RUN != 'true' + id: create-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ steps.issue.outputs.branch }} + TITLE: "🤖 Fix: ${{ steps.issue.outputs.title }}" + BODY: ${{ steps.read-description.outputs.body }} + COMMIT_MSG: "fix: ${{ steps.issue.outputs.title }} (#${{ steps.issue.outputs.number }})" + run: | + # Check if branch already exists and make unique if needed + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + echo "::warning::Branch $BRANCH already exists. Appending run ID." + BRANCH="${BRANCH}-${{ github.run_id }}" + fi + + # Create branch and commit + git checkout -b "$BRANCH" + git add -A + git commit -m "$COMMIT_MSG" + git push -u origin "$BRANCH" + + # Create PR + PR_URL=$(gh pr create \ + --title "$TITLE" \ + --body "$BODY" \ + --label "automated" \ + --label "automated-scan") + + # Extract PR number from URL + PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT + echo "✅ Created PR #$PR_NUMBER: $PR_URL" + + - name: Dry-run summary + if: steps.validate.outputs.has_changes == 'true' && env.DRY_RUN == 'true' + run: | + echo "🔍 DRY RUN - No PR created" + echo "" + echo "Would create PR with:" + echo " Branch: ${{ steps.issue.outputs.branch }}" + echo " Title: 🤖 Fix: ${{ steps.issue.outputs.title }}" + echo "" + echo "📝 Changes:" + git diff --stat + + # NOTE: Update 'maintainers' below to your team's slug (e.g., 'my-org/my-team') + - name: Request team review + if: steps.create-pr.outputs.pr_number + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const prNumber = ${{ steps.create-pr.outputs.pr_number }}; + let reviewRequested = false; + + // Request review from the team + // TODO: Replace 'maintainers' with your team slug + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + team_reviewers: ['maintainers'] + }); + console.log('✅ Requested review from maintainers team'); + reviewRequested = true; + } catch (error) { + console.log(`⚠️ Could not request team review: ${error.message}`); + + // Fallback: request review from issue author if they have access + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + reviewers: ['${{ steps.issue.outputs.author }}'] + }); + console.log('✅ Requested review from issue author @${{ steps.issue.outputs.author }}'); + reviewRequested = true; + } catch (e) { + console.log(`⚠️ Could not request individual review: ${e.message}`); + } + } + + // If no reviews requested, comment on PR to notify manually + if (!reviewRequested) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: '⚠️ Could not automatically request reviewers. Please manually assign reviewers to this PR.' + }); + console.log('ℹ️ Added comment asking for manual reviewer assignment'); + } + + - name: Comment on issue with PR link + if: steps.create-pr.outputs.pr_number + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const prNumber = ${{ steps.create-pr.outputs.pr_number }}; + const prUrl = '${{ steps.create-pr.outputs.pr_url }}'; + const issueNumber = ${{ env.ISSUE_NUMBER }}; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `✅ **I've created a fix for this issue!** + +🔗 **Pull Request:** #${prNumber} + +The PR has been submitted for review. Once approved and merged, this issue will be automatically closed. + +[View Pull Request](${prUrl})` + }); + + // Add rocket reaction + await github.rest.reactions.createForIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + content: 'rocket' + }); + + - name: Comment on issue if fix failed + if: steps.validate.outputs.has_changes != 'true' && env.DRY_RUN != 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const buildFailed = '${{ steps.validate.outputs.build_failed }}' === 'true'; + const issueNumber = ${{ env.ISSUE_NUMBER }}; + + let message = '❌ **I was unable to fix this issue automatically.**\n\n'; + + if (buildFailed) { + message += 'The changes I made did not compile. This issue may require manual intervention.\n\n'; + } else { + message += 'I analyzed the issue but couldn\'t determine the appropriate fix. This may require:\n'; + message += '- More context about the expected behavior\n'; + message += '- A human developer to investigate\n\n'; + } + + message += `[View workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: message + }); + + // Remove the agent-fix label since we couldn't fix it + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: 'agent-fix' + }).catch(() => {}); + + // Add needs-human label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['needs-human'] + }).catch(() => {}); + + - name: Cleanup + if: always() + run: | + rm -f issue_context.md files_stat.txt changes.diff || true diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index ff23a0ccf..60907ed06 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -5,15 +5,227 @@ on: types: [created] pull_request_review_comment: types: [created] + # Auto-trigger when PR becomes ready for review (supports forks) + pull_request_target: + types: [ready_for_review, opened, labeled] + # Trigger when someone submits a review on a PR + pull_request_review: + types: [submitted] permissions: - contents: read + contents: write pull-requests: write issues: write +# Prevent multiple reviews on the same PR +# Use multiple fallbacks for different event types +concurrency: + group: pr-review-${{ github.event.pull_request.number || github.event.issue.number || github.event.pull_request_review.pull_request.number || github.run_id }} + cancel-in-progress: false + jobs: # ========================================================================== - # MAIN REVIEW PIPELINE + # AUTOMATIC REVIEW FOR ORG MEMBERS + # Triggers when a PR is marked ready for review or opened (non-draft) + # Only runs for org members (supports fork-based workflow) + # ========================================================================== + auto-review: + if: | + github.event_name == 'pull_request_target' && + !github.event.pull_request.draft && + github.event.action != 'labeled' + runs-on: ubuntu-latest + + steps: + - name: Check if PR author is org member + id: membership + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + github-token: ${{ secrets.ORG_MEMBERSHIP_TOKEN }} + script: | + // Use the repository owner as the org (works for org repos) + const org = context.repo.owner; + 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 a repository secret named ORG_MEMBERSHIP_TOKEN:\n' + + ` gh secret set ORG_MEMBERSHIP_TOKEN --repo ${org}/${context.repo.repo}` + ); + } else { + core.setFailed(`Failed to check org membership: ${error.message}`); + } + } + + # SECURITY NOTE: Using pull_request_target to support fork-based workflows + # with access to secrets. This is safe because: + # 1. Org membership is verified BEFORE checkout (only org members trigger this) + # 2. The review-pr action only READS files (no code execution) + # 3. No shell scripts from the PR are executed + # For external contributors, reviews must be manually triggered via /review command + - name: Checkout PR head + if: steps.membership.outputs.is_member == 'true' + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Run PR Review Team + if: steps.membership.outputs.is_member == 'true' + uses: docker/cagent-action/review-pr@latest + with: + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + pr-number: ${{ github.event.pull_request.number }} + + # ========================================================================== + # AUTO-REVIEW FOR AUTOMATED SCAN PRs + # Triggers when a PR is labeled with 'automated-scan' + # SECURITY: Verifies the PR was created by our automation (github-actions[bot]) + # to prevent bypassing org membership check via label manipulation + # ========================================================================== + auto-review-scan-pr: + if: | + github.event_name == 'pull_request_target' && + github.event.action == 'labeled' && + github.event.label.name == 'automated-scan' + runs-on: ubuntu-latest + + steps: + - name: Verify PR author is automation + id: verify-author + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const allowedBots = ['github-actions[bot]', 'dependabot[bot]']; + + if (allowedBots.includes(author)) { + core.setOutput('is_automation', 'true'); + console.log(`✅ PR author ${author} is a trusted automation`); + } else { + core.setOutput('is_automation', 'false'); + console.log(`⚠️ PR author ${author} is not a trusted automation - skipping`); + console.log(` The 'automated-scan' label should only be used on PRs created by automation.`); + console.log(` For manual PRs, the standard org membership check applies.`); + } + + - name: Checkout repository + if: steps.verify-author.outputs.is_automation == 'true' + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Run PR Review Team + if: steps.verify-author.outputs.is_automation == 'true' + uses: docker/cagent-action/review-pr@latest + with: + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + pr-number: ${{ github.event.pull_request.number }} + + # ========================================================================== + # RESPOND TO REVIEW FEEDBACK ON AUTOMATED PRs + # When a reviewer comments on an automated scan PR, the agent either: + # - Makes the requested changes and pushes a new commit + # - Responds explaining why the change won't be made + # SECURITY: Verifies the PR was created by trusted automation + # ========================================================================== + respond-to-review: + if: | + github.event_name == 'pull_request_review' && + github.event.review.state == 'changes_requested' && + contains(github.event.pull_request.labels.*.name, 'automated-scan') + runs-on: ubuntu-latest + + steps: + - name: Verify PR author is automation + id: verify-author + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const allowedBots = ['github-actions[bot]', 'dependabot[bot]']; + + if (allowedBots.includes(author)) { + core.setOutput('is_automation', 'true'); + console.log(`✅ PR author ${author} is a trusted automation`); + } else { + core.setOutput('is_automation', 'false'); + core.warning(`PR has 'automated-scan' label but author ${author} is not a trusted automation. Skipping response.`); + } + + - name: Checkout PR branch + if: steps.verify-author.outputs.is_automation == 'true' + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + # Need write access to push changes + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + if: steps.verify-author.outputs.is_automation == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Respond to review feedback + if: steps.verify-author.outputs.is_automation == 'true' + id: respond + uses: docker/cagent-action@latest + with: + agent: ${{ github.workspace }}/.github/agents/review-responder.yaml + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + yolo: 'true' + timeout: 300 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REVIEW_BODY: ${{ github.event.review.body }} + REVIEW_ID: ${{ github.event.review.id }} + REVIEWER: ${{ github.event.review.user.login }} + + - name: Check for changes + if: steps.verify-author.outputs.is_automation == 'true' + id: changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Verify and push changes + if: steps.verify-author.outputs.is_automation == 'true' && steps.changes.outputs.has_changes == 'true' + run: | + # Verify build still works + if ! go build ./...; then + echo "::error::Changes do not compile. Discarding." + git checkout -- . + exit 1 + fi + + git add -A + git commit -m "fix: address review feedback from @${{ github.event.review.user.login }}" + git push + + # ========================================================================== + # MANUAL REVIEW PIPELINE + # Triggers when someone comments /review on a PR # ========================================================================== run-review: if: github.event.issue.pull_request && contains(github.event.comment.body, '/review') @@ -31,12 +243,16 @@ jobs: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # ========================================================================== - # LEARN FROM FEEDBACK - Process replies to agent review comments + # LEARN FROM FEEDBACK + # Processes replies to agent review comments for continuous improvement + # The learn action checks for marker to identify + # agent comments (more reliable than author-based detection) + # Memory is persisted via GitHub Actions cache and loaded by the review agent # ========================================================================== learn-from-feedback: - # 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: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 diff --git a/.github/workflows/weekly-codebase-scan.yml b/.github/workflows/weekly-codebase-scan.yml new file mode 100644 index 000000000..da3c41a40 --- /dev/null +++ b/.github/workflows/weekly-codebase-scan.yml @@ -0,0 +1,293 @@ +name: Weekly Codebase Scan + +on: + schedule: + # Run every Monday at 9am UTC + - cron: '0 9 * * 1' + workflow_dispatch: + inputs: + dry-run: + description: 'Analyze only, do not create PR' + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +# Only one scan at a time +concurrency: + group: weekly-codebase-scan + cancel-in-progress: false + +jobs: + scan-and-fix: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Run codebase scan + id: scan + uses: docker/cagent-action@latest + with: + agent: ${{ github.workspace }}/.github/agents/codebase-scanner.yaml + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + yolo: 'true' + timeout: 600 # 10 minutes - based on typical scan time of 5-7min + buffer + + - name: Validate changes (anti-hallucination check) + id: validate + run: | + OUTPUT_FILE="${{ steps.scan.outputs.output-file }}" + + # Check if any files were modified + CHANGED_FILES=$(git diff --name-only) + if [ -z "$CHANGED_FILES" ]; then + echo "✅ No changes made" + echo "has_valid_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "📝 Files modified by agent:" + echo "$CHANGED_FILES" + + # Check for unexpected new files (potential hallucination) + NEW_FILES=$(git diff --diff-filter=A --name-only) + if [ -n "$NEW_FILES" ]; then + echo "::warning::Agent created new files (unexpected for a scan):" + echo "$NEW_FILES" + echo "Reverting new files..." + echo "$NEW_FILES" | xargs -I {} git checkout HEAD -- "{}" 2>/dev/null || true + fi + + # Check for deleted files (potential hallucination) + DELETED_FILES=$(git diff --diff-filter=D --name-only) + if [ -n "$DELETED_FILES" ]; then + echo "::warning::Agent deleted files:" + echo "$DELETED_FILES" + # Only allow deletion of test files + for f in $DELETED_FILES; do + if [[ ! "$f" =~ _test\.go$ ]]; then + echo "::error::Agent deleted non-test file: $f - reverting" + git checkout -- "$f" + fi + done + fi + + # Validate each changed file exists and has valid changes + INVALID_COUNT=0 + VALID_COUNT=0 + + # Re-fetch changed files after reverting hallucinated ones + CHANGED_FILES=$(git diff --name-only) + + while IFS= read -r filepath; do + [ -z "$filepath" ] && continue + + if [ -f "$filepath" ]; then + # For Go files, do basic syntax check + if [[ "$filepath" =~ \.go$ ]]; then + if ! gofmt -e "$filepath" >/dev/null 2>&1; then + echo " ✗ INVALID (syntax error): $filepath" + git checkout -- "$filepath" + INVALID_COUNT=$((INVALID_COUNT + 1)) + continue + fi + fi + + echo " ✓ Valid: $filepath" + VALID_COUNT=$((VALID_COUNT + 1)) + else + echo " ✗ INVALID (file doesn't exist): $filepath" + INVALID_COUNT=$((INVALID_COUNT + 1)) + # Revert changes to non-existent files + git checkout -- "$filepath" 2>/dev/null || true + fi + done <<< "$CHANGED_FILES" + + echo "" + echo "Validation results:" + echo " - Valid changes: $VALID_COUNT" + echo " - Invalid changes (reverted): $INVALID_COUNT" + + if [ "$VALID_COUNT" -gt 0 ]; then + echo "has_valid_changes=true" >> $GITHUB_OUTPUT + else + echo "has_valid_changes=false" >> $GITHUB_OUTPUT + fi + + if [ "$INVALID_COUNT" -gt 0 ]; then + echo "::warning::Agent made changes to $INVALID_COUNT invalid files (hallucination). These have been reverted." + fi + + - name: Verify build + if: steps.validate.outputs.has_valid_changes == 'true' + run: | + echo "🔨 Verifying changes compile..." + # 5 minute timeout to prevent hangs from infinite loops + if ! timeout 300 go build ./...; then + if [ $? -eq 124 ]; then + echo "::error::Build timed out after 5 minutes. Reverting all changes." + else + echo "::error::Changes do not compile. Reverting all changes." + fi + git checkout -- . + echo "build_failed=true" >> $GITHUB_OUTPUT + exit 1 + fi + echo "✅ Build successful" + + - name: Check for changes + id: changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "📝 Final changes to commit:" + git status --porcelain + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "✅ No valid changes to commit" + fi + + - name: Gather change details + if: steps.changes.outputs.has_changes == 'true' && inputs.dry-run != true + id: gather + run: | + # Get list of changed files with stats + echo "📋 Gathering change details..." + + # Changed files list + git diff --stat > files_stat.txt + + # Full diff for analysis + git diff > changes.diff + + # Count stats + FILES_CHANGED=$(git diff --name-only | wc -l | tr -d ' ') + ADDITIONS=$(git diff --numstat | awk '{sum += $1} END {print sum+0}') + DELETIONS=$(git diff --numstat | awk '{sum += $2} END {print sum+0}') + + echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT + echo "additions=$ADDITIONS" >> $GITHUB_OUTPUT + echo "deletions=$DELETIONS" >> $GITHUB_OUTPUT + + echo "📊 Stats: $FILES_CHANGED files, +$ADDITIONS/-$DELETIONS lines" + + - name: Generate PR description + if: steps.changes.outputs.has_changes == 'true' && inputs.dry-run != true + id: describe + uses: docker/cagent-action@latest + with: + agent: agentcatalog/github-action-pr-description-generator + prompt: | + Generate a PR description for automated codebase scan fixes. + + **Context:** + This PR was automatically generated by a weekly codebase scan that looks for: + - Security vulnerabilities (injection, auth bypass, secrets) + - Logic errors (nil deref, ignored errors) + - Resource leaks (unclosed files, connections) + - Concurrency bugs (races, deadlocks) + + **Stats:** ${{ steps.gather.outputs.files_changed }} files changed, +${{ steps.gather.outputs.additions }}/-${{ steps.gather.outputs.deletions }} lines + + **Changed Files:** + $(cat files_stat.txt) + + **Diff:** + $(cat changes.diff) + + **Instructions:** + 1. Summarize what issues were found and fixed + 2. Group related changes together + 3. Explain WHY each change was made (the bug it fixes) + 4. Be concise but informative + 5. End with the standard footer (validation performed, review checklist) + + Format the description in GitHub Markdown. + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + timeout: 180 + + - name: Read generated description + if: steps.changes.outputs.has_changes == 'true' && inputs.dry-run != true + id: read-description + run: | + OUTPUT_FILE="${{ steps.describe.outputs.output-file }}" + if [ -f "$OUTPUT_FILE" ]; then + # Read the description, escaping for GitHub Actions + { + echo "body<> $GITHUB_OUTPUT + else + # Fallback description if generation failed + { + echo "body<> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' && inputs.dry-run != true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Use date-based branch to avoid conflicts with existing PRs + BRANCH="automated/weekly-codebase-scan-$(date +%Y%m%d)" + + # Check if branch already exists and append run ID if needed + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + echo "::warning::Branch $BRANCH already exists. Appending run ID." + BRANCH="${BRANCH}-${{ github.run_id }}" + fi + + # Create branch and commit + git checkout -b "$BRANCH" + git add -A + git commit -m "fix: automated codebase scan fixes" + git push -u origin "$BRANCH" + + # Create PR + PR_URL=$(gh pr create \ + --title "🔍 Weekly Codebase Scan - Automated Fixes" \ + --body "${{ steps.read-description.outputs.body }}" \ + --label "automated" \ + --label "automated-scan") + + echo "✅ Pull Request: $PR_URL" + + - name: Cleanup temporary files + if: always() + run: | + rm -f files_stat.txt changes.diff || true