allow reply-to-feedback in this repo #172
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Generate PR Description | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| jobs: | |
| generate-description: | |
| # 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 | |
| issues: write | |
| checks: write | |
| steps: | |
| - name: Check out Git repository | |
| uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 | |
| - name: Create check run | |
| id: create-check | |
| continue-on-error: true # Don't fail if checks: write permission is missing | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| with: | |
| script: | | |
| const prNumber = context.issue.number; | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const { data: check } = await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'Generate PR Description', | |
| head_sha: pr.head.sha, | |
| status: 'in_progress', | |
| started_at: new Date().toISOString() | |
| }); | |
| core.setOutput('check-id', check.id); | |
| # 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' | |
| continue-on-error: true # Don't fail workflow if token generation fails | |
| 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; | |
| // Validate PR number | |
| if (!Number.isInteger(prNumber) || prNumber < 1 || prNumber > 99999) { | |
| core.setFailed(`❌ Error: Invalid PR number (got: ${prNumber})`); | |
| return; | |
| } | |
| console.log(`✅ Validated PR number: ${prNumber}`); | |
| core.setOutput('pr_number', prNumber); | |
| // Add "eyes" reaction to comment | |
| try { | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: commentId, | |
| content: 'eyes' | |
| }); | |
| console.log('👀 Added reaction to comment'); | |
| } catch (error) { | |
| console.log(`⚠️ Warning: Could not add reaction: ${error.message}`); | |
| } | |
| - name: Get PR details | |
| 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 }}; | |
| try { | |
| // Get PR information | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| // Set outputs for PR metadata | |
| core.setOutput('title', pr.title); | |
| core.setOutput('branch', pr.head.ref); | |
| core.setOutput('base', pr.base.ref); | |
| core.setOutput('additions', pr.additions); | |
| core.setOutput('deletions', pr.deletions); | |
| core.setOutput('changed_files', pr.changed_files); | |
| console.log(`✅ Retrieved PR details for #${prNumber}`); | |
| console.log(`📊 Stats: ${pr.changed_files} files, +${pr.additions}/-${pr.deletions} lines`); | |
| // Get commit messages | |
| const { data: commits } = await github.rest.pulls.listCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const commitMessages = commits.map(commit => { | |
| const message = commit.commit.message.split('\n')[0]; | |
| const sha = commit.sha.substring(0, 7); | |
| return `- ${message} (${sha})`; | |
| }).join('\n'); | |
| fs.writeFileSync('commits.txt', commitMessages); | |
| // Get list of changed files with stats | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const filesList = files.map(file => | |
| `- \`${file.filename}\` (+${file.additions}/-${file.deletions}) - ${file.status}` | |
| ).join('\n'); | |
| fs.writeFileSync('files.txt', filesList); | |
| // Get the actual diff | |
| const { data: diff } = await github.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| mediaType: { | |
| format: 'diff' | |
| } | |
| }); | |
| fs.writeFileSync('pr.diff', diff); | |
| console.log(`📝 Generated commits.txt, files.txt, and pr.diff`); | |
| } catch (error) { | |
| core.setFailed(`❌ Failed to fetch PR details: ${error.message}`); | |
| } | |
| - name: Generate description with AI | |
| id: generate | |
| uses: ./ | |
| with: | |
| agent: agentcatalog/github-action-pr-description-generator | |
| prompt: | | |
| **PR Metadata:** | |
| - **Title:** ${{ steps.pr_details.outputs.title }} | |
| - **Branch:** ${{ steps.pr_details.outputs.branch }} → ${{ steps.pr_details.outputs.base }} | |
| - **PR Number:** #${{ steps.validate_pr.outputs.pr_number }} | |
| - **Stats:** ${{ steps.pr_details.outputs.changed_files }} files changed, +${{ steps.pr_details.outputs.additions }}/-${{ steps.pr_details.outputs.deletions }} lines | |
| **Commit Messages:** | |
| $(cat commits.txt) | |
| **Changed Files:** | |
| $(cat files.txt) | |
| **Diff:** | |
| $(cat pr.diff) | |
| anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github-token: ${{ steps.app-token.outputs.token || github.token }} | |
| timeout: 300 # 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 }}; | |
| const descriptionFile = '${{ steps.generate.outputs.output-file }}'; | |
| try { | |
| // Read generated description and extract cagent-output block if present. | |
| // The composite action should already clean this, but the agent may | |
| // emit conversational text before the code fence that slips through. | |
| const raw = fs.readFileSync(descriptionFile, 'utf8'); | |
| const fenceMatch = raw.match(/```cagent-output[\s\S]*?\n([\s\S]*?)```/); | |
| let description; | |
| if (fenceMatch) { | |
| description = fenceMatch[1].trim(); | |
| } else { | |
| // Fallback: extract from the first markdown heading onward | |
| // to avoid leaking tool call traces into the PR body | |
| const headingMatch = raw.match(/(^|\n)(## .+[\s\S]*)$/); | |
| description = headingMatch ? headingMatch[2].trim() : raw.trim(); | |
| } | |
| // Preserve issue-closing keywords from the existing PR body | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const existingBody = pr.body || ''; | |
| const closingLines = existingBody | |
| .split('\n') | |
| .filter(line => /^\s*(closes|fixes|resolves)\s*:?\s+/i.test(line)) | |
| .map(line => line.trim()); | |
| if (closingLines.length > 0) { | |
| // Only append lines not already present in the generated description | |
| const missing = closingLines.filter(line => !description.includes(line)); | |
| if (missing.length > 0) { | |
| description += '\n\n' + missing.join('\n'); | |
| } | |
| } | |
| // Update PR body | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| body: description | |
| }); | |
| console.log(`✅ Updated PR #${prNumber} with generated description`); | |
| } catch (error) { | |
| core.setFailed(`❌ Failed to update PR description: ${error.message}`); | |
| } | |
| - name: Post success comment | |
| 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 }}; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '✅ PR description has been generated and updated!' | |
| }); | |
| - name: Post failure comment | |
| 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 }}; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '❌ Failed to generate PR description. Check workflow logs for details.' | |
| }); | |
| - name: Post summary | |
| if: always() | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token || github.token }} | |
| script: | | |
| // Use context.issue.number directly - always available, doesn't depend on previous steps | |
| const prNumber = context.issue.number; | |
| const title = '${{ steps.pr_details.outputs.title }}' || 'Unknown'; | |
| const branch = '${{ steps.pr_details.outputs.branch }}' || 'Unknown'; | |
| const base = '${{ steps.pr_details.outputs.base }}' || 'Unknown'; | |
| const conclusion = '${{ steps.generate.conclusion }}' || 'failure'; | |
| const summary = [ | |
| '## 📝 PR Description Generator', | |
| '', | |
| `**PR:** #${prNumber}`, | |
| `**Title:** ${title}`, | |
| `**Branch:** ${branch} → ${base}`, | |
| '', | |
| conclusion === 'success' | |
| ? '**Result:** ✅ Description generated and PR updated' | |
| : '**Result:** ❌ Failed to generate description' | |
| ].join('\n'); | |
| await core.summary | |
| .addRaw(summary) | |
| .write(); | |
| - name: Cleanup temporary files | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| rm -f commits.txt files.txt pr.diff || true | |
| echo "🧹 Cleaned up temporary files" | |
| - name: Update check run | |
| if: always() && steps.create-check.outputs.check-id != '' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| CHECK_ID: ${{ steps.create-check.outputs.check-id }} | |
| JOB_STATUS: ${{ job.status }} | |
| with: | |
| script: | | |
| const conclusion = process.env.JOB_STATUS === 'cancelled' ? 'cancelled' : process.env.JOB_STATUS === 'success' ? 'success' : 'failure'; | |
| try { | |
| await github.rest.checks.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: parseInt(process.env.CHECK_ID, 10), | |
| status: 'completed', | |
| conclusion: conclusion, | |
| completed_at: new Date().toISOString() | |
| }); | |
| } catch (error) { | |
| core.warning(`Failed to update check run: ${error.message}`); | |
| } |