From 287ec85a3899c63914a87f2f7c8c4d3d7084906e Mon Sep 17 00:00:00 2001 From: offerpai <> Date: Tue, 26 May 2026 12:43:29 +0000 Subject: [PATCH] Initial commit --- .gitea/workflows/ai-review.yml | 193 +++++++++++++++++++++++++++++++++ README.md | 3 + 2 files changed, 196 insertions(+) create mode 100644 .gitea/workflows/ai-review.yml create mode 100644 README.md diff --git a/.gitea/workflows/ai-review.yml b/.gitea/workflows/ai-review.yml new file mode 100644 index 00000000..4e865e85 --- /dev/null +++ b/.gitea/workflows/ai-review.yml @@ -0,0 +1,193 @@ +name: AI Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + issues: write + +jobs: + ai-review: + runs-on: linux + timeout-minutes: 10 + steps: + - name: Review pull request diff + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_API_KEY_VAR: ${{ vars.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + OPENAI_BASE_URL_VAR: ${{ vars.OPENAI_BASE_URL }} + OPENAI_MODEL: ${{ secrets.OPENAI_MODEL }} + OPENAI_MODEL_VAR: ${{ vars.OPENAI_MODEL }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_API_URL: ${{ github.api_url }} + GITEA_REPOSITORY: ${{ github.repository }} + GITEA_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + shell: bash + run: | + set -euo pipefail + + OPENAI_API_KEY="${OPENAI_API_KEY:-${OPENAI_API_KEY_VAR:-}}" + OPENAI_BASE_URL="${OPENAI_BASE_URL:-${OPENAI_BASE_URL_VAR:-https://api.openai.com/v1}}" + OPENAI_MODEL="${OPENAI_MODEL:-${OPENAI_MODEL_VAR:-gpt-5}}" + OPENAI_BASE_URL="${OPENAI_BASE_URL%/}" + export OPENAI_API_KEY OPENAI_BASE_URL OPENAI_MODEL + + if [ -z "${OPENAI_API_KEY:-}" ]; then + echo "OPENAI_API_KEY is not configured. Add it in repository Settings -> Actions -> Secrets." + exit 1 + fi + + if [ -z "${GITEA_TOKEN:-}" ]; then + echo "GITEA_TOKEN is not available. Check Actions token permissions for this repository." + exit 1 + fi + + if [ -z "${PR_NUMBER:-}" ] || [ "${PR_NUMBER}" = "null" ]; then + echo "This workflow only reviews pull requests." + exit 0 + fi + + curl --fail-with-body -L \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Accept: text/plain" \ + "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff" \ + -o pr.diff + + diff_bytes="$(wc -c < pr.diff | tr -d ' ')" + if [ "${diff_bytes}" -eq 0 ]; then + printf 'No diff content found for PR #%s.\n' "${PR_NUMBER}" > review.md + should_fail=false + else + node <<'NODE' + const fs = require('fs'); + + const diff = fs.readFileSync('pr.diff', 'utf8'); + const title = process.env.PR_TITLE || ''; + const maxDiffChars = 120000; + const truncated = diff.length > maxDiffChars; + const inputDiff = truncated ? diff.slice(0, maxDiffChars) : diff; + + const payload = { + model: process.env.OPENAI_MODEL || 'gpt-5', + input: [ + { + role: 'system', + content: [ + { + type: 'input_text', + text: [ + 'You are a senior code reviewer for a Gitea pull request.', + 'Focus on correctness, security, data loss, concurrency, backward compatibility, and missing tests.', + 'Ignore formatting-only comments unless they hide a real bug.', + 'Return strict JSON only. Do not use markdown fences.', + 'Schema: {"summary": string, "findings": [{"severity": "critical"|"high"|"medium"|"low", "file": string, "line": number|null, "title": string, "detail": string}], "should_fail": boolean}.', + 'Set should_fail true only for critical or high confidence high severity issues.' + ].join(' ') + } + ] + }, + { + role: 'user', + content: [ + { + type: 'input_text', + text: `PR title: ${title}\nDiff was truncated: ${truncated}\n\n${inputDiff}` + } + ] + } + ] + }; + + fs.writeFileSync('openai-request.json', JSON.stringify(payload)); + NODE + + curl --fail-with-body "${OPENAI_BASE_URL}/responses" \ + -H "Authorization: Bearer ${OPENAI_API_KEY}" \ + -H "Content-Type: application/json" \ + -d @openai-request.json \ + -o openai-response.json + + node <<'NODE' + const fs = require('fs'); + + const response = JSON.parse(fs.readFileSync('openai-response.json', 'utf8')); + + function collectText(node, out = []) { + if (!node) return out; + if (typeof node === 'string') { + out.push(node); + } else if (Array.isArray(node)) { + for (const item of node) collectText(item, out); + } else if (typeof node === 'object') { + if (typeof node.output_text === 'string') out.push(node.output_text); + if (typeof node.text === 'string') out.push(node.text); + if (node.content) collectText(node.content, out); + if (node.output) collectText(node.output, out); + } + return out; + } + + const raw = collectText(response).join('\n').trim(); + let review; + try { + review = JSON.parse(raw); + } catch { + review = { + summary: raw || 'AI review returned no readable output.', + findings: [], + should_fail: false + }; + } + + const findings = Array.isArray(review.findings) ? review.findings : []; + const lines = []; + lines.push('## AI Review'); + lines.push(''); + lines.push(review.summary || 'Review completed.'); + lines.push(''); + + if (findings.length === 0) { + lines.push('No high-confidence issues found.'); + } else { + lines.push('Findings:'); + for (const finding of findings.slice(0, 20)) { + const severity = finding.severity || 'medium'; + const file = finding.file || 'unknown file'; + const line = finding.line ? `:${finding.line}` : ''; + const title = finding.title || 'Potential issue'; + const detail = finding.detail || ''; + lines.push(`- [${severity}] ${file}${line} - ${title}`); + if (detail) lines.push(` ${detail}`); + } + } + + fs.writeFileSync('review.md', lines.join('\n') + '\n'); + fs.writeFileSync('should-fail.txt', review.should_fail ? 'true' : 'false'); + NODE + + should_fail="$(cat should-fail.txt)" + fi + + node <<'NODE' + const fs = require('fs'); + fs.writeFileSync('comment.json', JSON.stringify({ + body: fs.readFileSync('review.md', 'utf8') + })); + NODE + + curl --fail-with-body -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @comment.json \ + "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments" + + if [ "${should_fail}" = "true" ]; then + echo "AI review found high severity issues." + exit 1 + fi diff --git a/README.md b/README.md new file mode 100644 index 00000000..06211095 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ai-review-template + +Template repository with Gitea Actions AI pull request review workflow. \ No newline at end of file