Initial commit
This commit is contained in:
commit
d4d2ac5d8f
8 changed files with 293 additions and 0 deletions
21
.forgejo/workflows/ai-pr-review.yml
Normal file
21
.forgejo/workflows/ai-pr-review.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
name: AI PR Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
review:
|
||||||
|
runs-on: js-ubuntu-22
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.AI_API_URL }}
|
||||||
|
api_key: ${{ secrets.AI_API_KEY }}
|
||||||
|
# model: openai/gpt-oss-120b # optional, this is the default
|
||||||
69
action.yml
Normal file
69
action.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
name: AI PR Review
|
||||||
|
description: Automated AI-powered PR code review using any OpenAI-compatible endpoint
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
api_url:
|
||||||
|
description: Base URL of the OpenAI-compatible API endpoint (e.g. https://api.openai.com/v1)
|
||||||
|
required: true
|
||||||
|
api_key:
|
||||||
|
description: API key for the endpoint
|
||||||
|
required: true
|
||||||
|
model:
|
||||||
|
description: Model identifier to use for the review
|
||||||
|
required: false
|
||||||
|
default: openai/gpt-oss-120b
|
||||||
|
github_token:
|
||||||
|
description: Token used to post the review comment and fetch PR comments
|
||||||
|
required: false
|
||||||
|
default: ${{ github.token }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch previous review context
|
||||||
|
id: context
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ inputs.github_token }}
|
||||||
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: bash "${{ github.action_path }}/scripts/fetch-context.sh"
|
||||||
|
|
||||||
|
- name: Generate diff
|
||||||
|
id: diff
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PREV_SHA: ${{ steps.context.outputs.prev_sha }}
|
||||||
|
run: bash "${{ github.action_path }}/scripts/generate-diff.sh"
|
||||||
|
|
||||||
|
- name: Run AI review
|
||||||
|
id: ai_review
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AI_API_URL: ${{ inputs.api_url }}
|
||||||
|
AI_API_KEY: ${{ inputs.api_key }}
|
||||||
|
AI_MODEL: ${{ inputs.model }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
|
PREV_REVIEW: ${{ steps.context.outputs.prev_review }}
|
||||||
|
INCREMENTAL: ${{ steps.diff.outputs.incremental }}
|
||||||
|
run: bash "${{ github.action_path }}/scripts/run-review.sh"
|
||||||
|
|
||||||
|
- name: Post review comment
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ inputs.github_token }}
|
||||||
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
REVIEW: ${{ steps.ai_review.outputs.review }}
|
||||||
|
CURRENT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
TRUNCATED: ${{ steps.diff.outputs.truncated }}
|
||||||
|
INCREMENTAL: ${{ steps.diff.outputs.incremental }}
|
||||||
|
run: bash "${{ github.action_path }}/scripts/post-comment.sh"
|
||||||
9
prompts/review-update.md
Normal file
9
prompts/review-update.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
You are an expert code reviewer continuing a review of a pull request that has been updated.
|
||||||
|
|
||||||
|
Your previous review is included as context. Focus on:
|
||||||
|
- What has changed since your last review
|
||||||
|
- Whether previously raised issues have been addressed
|
||||||
|
- Any new correctness, security, or performance concerns introduced by the new changes
|
||||||
|
- New edge cases or missing tests
|
||||||
|
|
||||||
|
Structure your updated review with: **Summary of Changes**, **Resolved Issues** (from your prior review), **New Issues** (critical/minor), and **Suggestions**. Be concise and avoid repeating feedback that is no longer relevant.
|
||||||
10
prompts/review.md
Normal file
10
prompts/review.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
You are an expert code reviewer. Review the following pull request and provide concise, actionable feedback.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- Correctness and potential bugs
|
||||||
|
- Security issues
|
||||||
|
- Performance concerns
|
||||||
|
- Code clarity and maintainability
|
||||||
|
- Missing tests or edge cases
|
||||||
|
|
||||||
|
Structure your review with sections: **Summary**, **Issues** (critical/minor), and **Suggestions**. Be concise.
|
||||||
34
scripts/fetch-context.sh
Normal file
34
scripts/fetch-context.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetches the previous AI review comment from the PR (if any).
|
||||||
|
# Required env vars: GITEA_TOKEN, GITEA_SERVER_URL, REPO, PR_NUMBER
|
||||||
|
# Writes to $GITHUB_OUTPUT: prev_sha, prev_review
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
COMMENTS=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${GITEA_SERVER_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/comments")
|
||||||
|
|
||||||
|
# Find the most recent comment that contains our SHA marker
|
||||||
|
PREV_COMMENT=$(echo "$COMMENTS" | jq -r '
|
||||||
|
[ .[] | select(.body | contains("<!-- ai-review-sha:")) ] | last | .body // ""
|
||||||
|
')
|
||||||
|
|
||||||
|
if [ -z "$PREV_COMMENT" ]; then
|
||||||
|
echo "No previous AI review found — will run a full review."
|
||||||
|
echo "prev_sha=" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "prev_review=" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PREV_SHA=$(echo "$PREV_COMMENT" | grep -oP '(?<=<!-- ai-review-sha:)[^>]+(?= -->)' || true)
|
||||||
|
# Strip the trailing marker line to get just the review body
|
||||||
|
PREV_REVIEW=$(echo "$PREV_COMMENT" | sed '/<!-- ai-review-sha:/d')
|
||||||
|
|
||||||
|
echo "Found previous review at commit ${PREV_SHA}"
|
||||||
|
|
||||||
|
echo "prev_sha=${PREV_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "prev_review<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "$PREV_REVIEW" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
26
scripts/generate-diff.sh
Normal file
26
scripts/generate-diff.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Required env vars: (none)
|
||||||
|
# Optional env vars: PREV_SHA (if set, generates an incremental diff from that commit)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git fetch origin main --depth=100
|
||||||
|
|
||||||
|
if [ -n "${PREV_SHA:-}" ] && git cat-file -e "${PREV_SHA}^{commit}" 2>/dev/null; then
|
||||||
|
echo "Generating incremental diff from ${PREV_SHA} to HEAD"
|
||||||
|
git diff "${PREV_SHA}...HEAD" > pr.diff
|
||||||
|
echo "incremental=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "Generating full diff from origin/main to HEAD"
|
||||||
|
git diff origin/main...HEAD > pr.diff
|
||||||
|
echo "incremental=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIFF_SIZE=$(wc -c < pr.diff)
|
||||||
|
echo "Diff size: ${DIFF_SIZE} bytes"
|
||||||
|
|
||||||
|
if [ "$DIFF_SIZE" -gt 12288 ]; then
|
||||||
|
head -c 12288 pr.diff > pr_truncated.diff
|
||||||
|
printf '\n\n[... diff truncated due to size ...]' >> pr_truncated.diff
|
||||||
|
mv pr_truncated.diff pr.diff
|
||||||
|
echo "truncated=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
29
scripts/post-comment.sh
Normal file
29
scripts/post-comment.sh
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Required env vars: GITEA_TOKEN, GITEA_SERVER_URL, REPO, PR_NUMBER, REVIEW, CURRENT_SHA
|
||||||
|
# Optional env vars: TRUNCATED, INCREMENTAL
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FOOTER=""
|
||||||
|
if [ "${TRUNCATED:-}" = "true" ]; then
|
||||||
|
FOOTER="\n\n> **Note:** The diff was truncated to 12 KB. Some changes may not have been reviewed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
LABEL="## AI Code Review"
|
||||||
|
if [ "${INCREMENTAL:-false}" = "true" ]; then
|
||||||
|
LABEL="## AI Code Review (updated)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Embed the current SHA as a hidden marker so future runs can find it
|
||||||
|
BODY=$(jq -n \
|
||||||
|
--arg label "$LABEL" \
|
||||||
|
--arg review "$REVIEW" \
|
||||||
|
--arg footer "$FOOTER" \
|
||||||
|
--arg sha "$CURRENT_SHA" \
|
||||||
|
'{ body: ($label + "\n\n" + $review + $footer + "\n\n---\n*Automated review powered by AI. Always verify suggestions before applying.*\n<!-- ai-review-sha:" + $sha + " -->") }')
|
||||||
|
|
||||||
|
curl -sf \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$BODY" \
|
||||||
|
"${GITEA_SERVER_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/comments"
|
||||||
95
scripts/run-review.sh
Normal file
95
scripts/run-review.sh
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Required env vars: AI_API_URL, AI_API_KEY, AI_MODEL, PR_TITLE, PR_BODY
|
||||||
|
# Optional env vars: PREV_REVIEW, INCREMENTAL
|
||||||
|
# Reads: pr.diff, prompts/review.md (or prompts/review-update.md for incremental)
|
||||||
|
# Writes: $GITHUB_OUTPUT (review)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROMPTS_DIR="${SCRIPT_DIR}/../prompts"
|
||||||
|
|
||||||
|
DIFF=$(cat pr.diff)
|
||||||
|
echo "Diff size: $(wc -c < pr.diff) bytes"
|
||||||
|
|
||||||
|
if [ "${INCREMENTAL:-false}" = "true" ] && [ -n "${PREV_REVIEW:-}" ]; then
|
||||||
|
echo "Building incremental review with conversation history"
|
||||||
|
SYSTEM_PROMPT=$(cat "${PROMPTS_DIR}/review-update.md")
|
||||||
|
|
||||||
|
# Reconstruct conversation: the model previously reviewed an earlier state,
|
||||||
|
# now we show it only what changed since then.
|
||||||
|
MESSAGES=$(jq -n \
|
||||||
|
--arg system "$SYSTEM_PROMPT" \
|
||||||
|
--arg prev "$PREV_REVIEW" \
|
||||||
|
--arg title "$PR_TITLE" \
|
||||||
|
--arg body "$PR_BODY" \
|
||||||
|
--arg diff "$DIFF" \
|
||||||
|
'[
|
||||||
|
{ role: "system", content: $system },
|
||||||
|
{ role: "assistant", content: $prev },
|
||||||
|
{ role: "user", content: ("The PR has been updated. Here are the new changes since your last review.\n\nPR Title: " + $title + "\nPR Description: " + $body + "\n\nIncremental diff:\n```diff\n" + $diff + "\n```\n\nUpdate your review, keeping previous feedback relevant to unchanged code and focusing new commentary on what has changed.") }
|
||||||
|
]')
|
||||||
|
else
|
||||||
|
echo "Building full review"
|
||||||
|
SYSTEM_PROMPT=$(cat "${PROMPTS_DIR}/review.md")
|
||||||
|
|
||||||
|
MESSAGES=$(jq -n \
|
||||||
|
--arg system "$SYSTEM_PROMPT" \
|
||||||
|
--arg title "$PR_TITLE" \
|
||||||
|
--arg body "$PR_BODY" \
|
||||||
|
--arg diff "$DIFF" \
|
||||||
|
'[
|
||||||
|
{ role: "system", content: $system },
|
||||||
|
{ role: "user", content: ("PR Title: " + $title + "\nPR Description: " + $body + "\n\nDiff:\n```diff\n" + $diff + "\n```") }
|
||||||
|
]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg model "$AI_MODEL" \
|
||||||
|
--argjson messages "$MESSAGES" \
|
||||||
|
'{
|
||||||
|
model: $model,
|
||||||
|
messages: $messages,
|
||||||
|
max_tokens: 1024,
|
||||||
|
temperature: 0.2
|
||||||
|
}')
|
||||||
|
|
||||||
|
echo "Payload size: $(echo "$PAYLOAD" | wc -c) bytes"
|
||||||
|
echo "Calling endpoint: ${AI_API_URL%/}/chat/completions"
|
||||||
|
echo "Model: ${AI_MODEL}"
|
||||||
|
|
||||||
|
HTTP_RESPONSE=$(curl -s \
|
||||||
|
--max-time 120 \
|
||||||
|
-w "\n__HTTP_STATUS__:%{http_code}" \
|
||||||
|
-H "Authorization: Bearer ${AI_API_KEY}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${AI_API_URL%/}/chat/completions")
|
||||||
|
CURL_EXIT=$?
|
||||||
|
|
||||||
|
HTTP_STATUS=$(echo "$HTTP_RESPONSE" | grep '__HTTP_STATUS__:' | cut -d: -f2)
|
||||||
|
RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '/__HTTP_STATUS__:/d')
|
||||||
|
|
||||||
|
echo "curl exit code: ${CURL_EXIT}"
|
||||||
|
echo "HTTP status: ${HTTP_STATUS}"
|
||||||
|
echo "Response body: ${RESPONSE_BODY}"
|
||||||
|
|
||||||
|
if [ "$CURL_EXIT" -ne 0 ]; then
|
||||||
|
echo "::error::curl failed with exit code ${CURL_EXIT}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
|
||||||
|
echo "::error::API returned HTTP ${HTTP_STATUS}: ${RESPONSE_BODY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REVIEW=$(echo "$RESPONSE_BODY" | jq -r '.choices[0].message.content')
|
||||||
|
if [ -z "$REVIEW" ] || [ "$REVIEW" = "null" ]; then
|
||||||
|
echo "::error::Failed to extract review from response. Full body: ${RESPONSE_BODY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Review length: $(echo "$REVIEW" | wc -c) chars"
|
||||||
|
echo "review<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "$REVIEW" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue