-
Notifications
You must be signed in to change notification settings - Fork 0
feat(actions): add reusable code-review composite action #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| name: 'protoLabs Code Review' | ||
| description: 'Async-parallel code review via the protoLabs LLM gateway. Maps the repo into bounded feature records, runs a model-backed review, posts findings as a sticky PR comment, and appends them to the GitHub Actions step summary. Non-blocking by design.' | ||
| author: 'protoLabs' | ||
| branding: | ||
| icon: 'eye' | ||
| color: 'blue' | ||
|
|
||
| inputs: | ||
| gateway-api-key: | ||
| description: 'Bearer token for the LLM gateway (passed as OPENAI_API_KEY to review-code).' | ||
| required: true | ||
| gateway-base-url: | ||
| description: 'LLM gateway base URL without `/v1`. `/v1` is appended automatically.' | ||
| required: false | ||
| default: 'https://api.proto-labs.ai' | ||
| model: | ||
| description: 'Gateway model alias. Defaults to review-code default (protolabs/smart).' | ||
| required: false | ||
| default: '' | ||
| timeout-ms: | ||
| description: 'Per-request timeout for the LLM gateway in milliseconds.' | ||
| required: false | ||
| default: '300000' | ||
| github-token: | ||
| description: 'Token used to post the sticky PR comment. Defaults to the workflow GITHUB_TOKEN.' | ||
| required: false | ||
| default: ${{ github.token }} | ||
| pr-number: | ||
| description: 'Pull request number to comment on. Defaults to github.event.pull_request.number.' | ||
| required: false | ||
| default: ${{ github.event.pull_request.number }} | ||
| release-tools-ref: | ||
| description: 'release-tools ref (branch/tag/commit) to install. Defaults to main.' | ||
| required: false | ||
| default: 'main' | ||
|
|
||
| outputs: | ||
| findings: | ||
| description: 'Number of review findings produced. Empty string if the review step failed.' | ||
| value: ${{ steps.review.outputs.findings }} | ||
| report-path: | ||
| description: 'Filesystem path of the generated markdown report. Empty string if no report was generated.' | ||
| value: ${{ steps.report.outputs.report_path }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Initialize review state | ||
| shell: bash | ||
| run: | | ||
| npx --yes -p "github:protoLabsAI/release-tools#${{ inputs.release-tools-ref }}" review-code init | ||
|
|
||
| - name: Map features | ||
| shell: bash | ||
| run: | | ||
| npx --yes -p "github:protoLabsAI/release-tools#${{ inputs.release-tools-ref }}" review-code map | ||
|
|
||
| - name: Run review against PR diff | ||
| id: review | ||
| shell: bash | ||
| continue-on-error: true | ||
| env: | ||
| OPENAI_API_KEY: ${{ inputs.gateway-api-key }} | ||
| OPENAI_BASE_URL: ${{ inputs.gateway-base-url }}/v1 | ||
| CODE_REVIEW_TIMEOUT_MS: ${{ inputs.timeout-ms }} | ||
| CODE_REVIEW_MODEL: ${{ inputs.model }} | ||
| run: | | ||
| set -o pipefail | ||
| ARGS="--all" | ||
| if [ -n "${{ inputs.model }}" ]; then ARGS="$ARGS --model ${{ inputs.model }}"; fi | ||
| npx --yes -p "github:protoLabsAI/release-tools#${{ inputs.release-tools-ref }}" review-code run $ARGS 2>&1 | tee /tmp/review-run.log | ||
| findings=$(grep -oE 'findings=[0-9]+' /tmp/review-run.log | tail -1 | cut -d= -f2) | ||
| echo "findings=${findings:-0}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Build markdown report | ||
| id: report | ||
| if: always() | ||
| shell: bash | ||
| run: | | ||
| npx --yes -p "github:protoLabsAI/release-tools#${{ inputs.release-tools-ref }}" review-code report --output /tmp/code-review.md 2>&1 | tee /tmp/report.log || true | ||
| if [ -f /tmp/code-review.md ]; then | ||
| echo "report_path=/tmp/code-review.md" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Post findings as sticky PR comment | ||
| if: always() && steps.report.outputs.report_path != '' && inputs.pr-number != '' | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ inputs.github-token }} | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| REPORT_PATH: ${{ steps.report.outputs.report_path }} | ||
| FINDINGS_COUNT: ${{ steps.review.outputs.findings }} | ||
| run: node "${{ github.action_path }}/post-findings.mjs" | ||
|
|
||
| - name: Append findings to step summary | ||
| if: always() && steps.report.outputs.report_path != '' | ||
| shell: bash | ||
| run: | | ||
| { | ||
| echo "## Code Review — ${{ steps.review.outputs.findings }} finding(s)" | ||
| echo | ||
| cat "${{ steps.report.outputs.report_path }}" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Post a code-review findings report as a sticky PR comment. | ||
| * | ||
| * Reads REPORT_PATH, PR_NUMBER, FINDINGS_COUNT, and GH_TOKEN from the | ||
| * environment. Uses the `gh` CLI to PATCH a previously-posted comment | ||
| * tagged with the `code-review:findings` marker, so re-runs replace | ||
| * prior output instead of stacking new comments on the PR. | ||
| * | ||
| * Called from the composite action at | ||
| * `.github/actions/code-review/action.yml`. Designed to be non-blocking: | ||
| * any failure is logged and exit code is 0. | ||
| */ | ||
| import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { join } from 'node:path'; | ||
| import { tmpdir } from 'node:os'; | ||
|
|
||
| const MARKER = '<!-- code-review:findings -->'; | ||
|
|
||
| const reportPath = process.env.REPORT_PATH; | ||
| const prNumber = process.env.PR_NUMBER; | ||
| const findingsCount = process.env.FINDINGS_COUNT || '?'; | ||
|
|
||
| if (!reportPath || !prNumber) { | ||
| console.error('REPORT_PATH and PR_NUMBER must be set'); | ||
| process.exit(0); // non-blocking | ||
| } | ||
|
|
||
| let report; | ||
| try { | ||
| report = readFileSync(reportPath, 'utf8'); | ||
| } catch (err) { | ||
| console.error(`Cannot read report at ${reportPath}: ${err.message}`); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| const body = `${MARKER} | ||
| ## Code Review — ${findingsCount} finding(s) | ||
|
|
||
| > Async review running parallel to CodeRabbit. Findings are advisory; not all are merge blockers. | ||
|
|
||
| ${report} | ||
| `; | ||
|
|
||
| function gh(args, opts = {}) { | ||
| return execFileSync('gh', args, { encoding: 'utf8', ...opts }); | ||
| } | ||
|
|
||
| const workdir = mkdtempSync(join(tmpdir(), 'code-review-')); | ||
| const bodyFile = join(workdir, 'body.md'); | ||
| writeFileSync(bodyFile, body); | ||
|
|
||
| let existingId = null; | ||
| try { | ||
| const comments = JSON.parse(gh(['pr', 'view', prNumber, '--json', 'comments'])); | ||
| const match = (comments.comments ?? []).find((c) => c.body?.includes(MARKER)); | ||
| if (match) existingId = match.id; | ||
|
Comment on lines
+56
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusive🏁 Script executed: #!/bin/bash
# Verify whether comment IDs from `gh pr view --json comments` are REST-patchable IDs.
set -euo pipefail
PR_NUMBER="${PR_NUMBER:?set PR_NUMBER}"
# Shows the shape of IDs returned by pr view
gh pr view "$PR_NUMBER" --json comments --jq '.comments[] | {id, url}' | head -n 5
# Shows numeric IDs from REST issue comments for the same PR
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --jq '.[] | {id, html_url}' | head -n 5Repository: protoLabsAI/release-tools Length of output: 113 Confirm
Proposed fix (lookup via REST comments so IDs are patchable)-let existingId = null;
+let existingId = null;
try {
- const comments = JSON.parse(gh(['pr', 'view', prNumber, '--json', 'comments']));
- const match = (comments.comments ?? []).find((c) => c.body?.includes(MARKER));
+ const comments = JSON.parse(
+ gh([
+ 'api',
+ `repos/${process.env.GITHUB_REPOSITORY}/issues/${prNumber}/comments`,
+ '--paginate',
+ ]),
+ );
+ const match = (comments ?? []).find((c) => c.body?.includes(MARKER));
if (match) existingId = match.id;
} catch (err) {
console.error(`Failed to list comments (continuing as new): ${err.message}`);
}🤖 Prompt for AI Agents |
||
| } catch (err) { | ||
| console.error(`Failed to list comments (continuing as new): ${err.message}`); | ||
| } | ||
|
|
||
| if (existingId) { | ||
| try { | ||
| const payloadFile = join(workdir, 'patch.json'); | ||
| writeFileSync(payloadFile, JSON.stringify({ body })); | ||
| gh([ | ||
| 'api', | ||
| '--method', | ||
| 'PATCH', | ||
| `/repos/{owner}/{repo}/issues/comments/${existingId}`, | ||
| '--input', | ||
| payloadFile, | ||
| ]); | ||
| console.log(`Updated existing code-review comment ${existingId}`); | ||
| process.exit(0); | ||
| } catch (err) { | ||
| console.error(`Patch failed, posting new comment instead: ${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| gh(['pr', 'comment', prNumber, '--body-file', bodyFile]); | ||
| console.log('Posted new code-review comment'); | ||
| } catch (err) { | ||
| console.error(`Failed to post comment: ${err.message}`); | ||
| process.exit(0); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid stale report reuse by using a unique temp file per run.
Line 80 writes to a fixed
/tmp/code-review.mdand Line 81 only checks existence. On persistent runners, a previous file can be reused if report generation fails, leading to stale findings being posted.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents