From d64745991b1ffad9daa7fdb64a21bf31939e002d Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Tue, 19 Aug 2025 10:09:30 +0300 Subject: [PATCH] Add Stage 1 workflow for external PR info collection - Collects PR information without requiring secrets - Triggers on pull_request events and @claude-review-ext comments - Uploads PR details as artifact for secure processing --- .../workflows/claude-review-ext-stage1.yml | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .github/workflows/claude-review-ext-stage1.yml diff --git a/.github/workflows/claude-review-ext-stage1.yml b/.github/workflows/claude-review-ext-stage1.yml new file mode 100644 index 00000000..fa193dcb --- /dev/null +++ b/.github/workflows/claude-review-ext-stage1.yml @@ -0,0 +1,231 @@ +name: Claude Review External - Stage 1 (PR Info Collection) + +# This workflow runs on pull requests from forks +# It collects PR information and saves it as an artifact for Stage 2 +# No secrets are available or needed in this stage + +on: + pull_request: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + collect-pr-info: + # Only trigger on specific conditions + # For PRs: always collect info + # For comments: only when @claude-review-ext is mentioned + if: | + github.event_name == 'pull_request' || + ( + (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + contains(github.event.comment.body, '@claude-review-ext') + ) + + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: read + + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + # For PRs, this automatically checks out the merge commit + fetch-depth: 0 + + - name: Collect PR Information + id: pr-info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + let prNumber, prTitle, prBody, prAuthor, baseBranch, headBranch, headSha; + let triggerPhrase = ''; + let commentBody = ''; + let commentId = null; + let commentAuthor = ''; + + if (context.eventName === 'pull_request') { + prNumber = context.payload.pull_request.number; + prTitle = context.payload.pull_request.title; + prBody = context.payload.pull_request.body || ''; + prAuthor = context.payload.pull_request.user.login; + baseBranch = context.payload.pull_request.base.ref; + headBranch = context.payload.pull_request.head.ref; + headSha = context.payload.pull_request.head.sha; + } else if (context.eventName === 'issue_comment') { + // For issue comments on PRs + const issue = context.payload.issue; + if (!issue.pull_request) { + console.log('Not a PR comment, skipping'); + return; + } + + prNumber = issue.number; + triggerPhrase = '@claude-review-ext'; + commentBody = context.payload.comment.body; + commentId = context.payload.comment.id; + commentAuthor = context.payload.comment.user.login; + + // Fetch full PR details + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + prTitle = pr.title; + prBody = pr.body || ''; + prAuthor = pr.user.login; + baseBranch = pr.base.ref; + headBranch = pr.head.ref; + headSha = pr.head.sha; + } else if (context.eventName === 'pull_request_review_comment') { + prNumber = context.payload.pull_request.number; + prTitle = context.payload.pull_request.title; + prBody = context.payload.pull_request.body || ''; + prAuthor = context.payload.pull_request.user.login; + baseBranch = context.payload.pull_request.base.ref; + headBranch = context.payload.pull_request.head.ref; + headSha = context.payload.pull_request.head.sha; + triggerPhrase = '@claude-review-ext'; + commentBody = context.payload.comment.body; + commentId = context.payload.comment.id; + commentAuthor = context.payload.comment.user.login; + } + + // Get list of changed files + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const changedFiles = files.map(f => ({ + filename: f.filename, + status: f.status, + additions: f.additions, + deletions: f.deletions, + changes: f.changes, + patch: f.patch + })); + + // Get diff + const { data: diff } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + mediaType: { + format: 'diff' + } + }); + + const prInfo = { + prNumber, + prTitle, + prBody, + prAuthor, + baseBranch, + headBranch, + headSha, + triggerPhrase, + commentBody, + commentId, + commentAuthor, + changedFiles, + diff, + eventName: context.eventName, + repository: `${context.repo.owner}/${context.repo.repo}`, + triggeredAt: new Date().toISOString() + }; + + // Save to file + fs.writeFileSync('pr-info.json', JSON.stringify(prInfo, null, 2)); + + console.log(`Collected info for PR #${prNumber} by ${prAuthor}`); + console.log(`Changed files: ${changedFiles.length}`); + console.log(`Event: ${context.eventName}`); + if (triggerPhrase) { + console.log(`Triggered by: ${commentAuthor} with phrase: ${triggerPhrase}`); + } + + // Set outputs for job summary + core.setOutput('pr_number', prNumber); + core.setOutput('pr_author', prAuthor); + core.setOutput('changed_files_count', changedFiles.length); + + - name: Run Basic Validation + id: validation + run: | + echo "Running basic validation checks..." + + # Check if this is from a fork + IS_FORK="${{ github.event.pull_request.head.repo.fork }}" + echo "Is from fork: $IS_FORK" + + # Count files changed + CHANGED_FILES=$(jq '.changedFiles | length' pr-info.json) + echo "Files changed: $CHANGED_FILES" + + # Check file types + echo "File types changed:" + jq -r '.changedFiles[].filename' pr-info.json | sed 's/.*\.//' | sort | uniq -c + + # Check for potential issues + LARGE_DIFF=false + if [ "$CHANGED_FILES" -gt 100 ]; then + LARGE_DIFF=true + echo "⚠️ Large PR detected: $CHANGED_FILES files changed" + fi + + echo "is_fork=$IS_FORK" >> $GITHUB_OUTPUT + echo "large_diff=$LARGE_DIFF" >> $GITHUB_OUTPUT + + - name: Upload PR Information + uses: actions/upload-artifact@v4 + with: + name: pr-info-${{ github.event.pull_request.number || github.event.issue.number }} + path: pr-info.json + retention-days: 1 + + - name: Post Status Comment + if: github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.pr-info.outputs.pr_number }}; + const isFork = '${{ steps.validation.outputs.is_fork }}' === 'true'; + const filesCount = ${{ steps.pr-info.outputs.changed_files_count }}; + + let statusMessage = `🔄 **Claude Review Stage 1 Complete**\n\n`; + statusMessage += `- PR: #${prNumber}\n`; + statusMessage += `- Author: @${{ steps.pr-info.outputs.pr_author }}\n`; + statusMessage += `- Files Changed: ${filesCount}\n`; + statusMessage += `- Fork Status: ${isFork ? '🔱 External Fork' : '✅ Direct Branch'}\n\n`; + statusMessage += `📋 Stage 2 (Claude Review) will run automatically after this workflow completes.\n`; + statusMessage += `This two-stage process ensures secure handling of forked PRs.`; + + // Post comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: statusMessage + }); + + - name: Job Summary + run: | + echo "## Claude Review Stage 1 Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **PR Number**: #${{ steps.pr-info.outputs.pr_number }}" >> $GITHUB_STEP_SUMMARY + echo "- **Author**: @${{ steps.pr-info.outputs.pr_author }}" >> $GITHUB_STEP_SUMMARY + echo "- **Files Changed**: ${{ steps.pr-info.outputs.changed_files_count }}" >> $GITHUB_STEP_SUMMARY + echo "- **From Fork**: ${{ steps.validation.outputs.is_fork }}" >> $GITHUB_STEP_SUMMARY + echo "- **Large PR**: ${{ steps.validation.outputs.large_diff }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ PR information collected and uploaded as artifact for Stage 2 processing." >> $GITHUB_STEP_SUMMARY \ No newline at end of file