Skip to content

Commit cb54173

Browse files
authored
Codex AI Reviewer (#295)
* Codex AI Reviewer * Remove contributors restriction * Add back restriction * Add back check
1 parent 91a3e9c commit cb54173

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
name: PR → Codex review → Slack
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, ready_for_review]
6+
7+
jobs:
8+
codex_review:
9+
# Run only for trusted contributors
10+
if: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association) }}
11+
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 15
14+
permissions:
15+
contents: read
16+
pull-requests: write
17+
18+
steps:
19+
- name: Checkout PR HEAD (full history)
20+
uses: actions/checkout@v4
21+
with:
22+
ref: ${{ github.event.pull_request.head.sha }}
23+
fetch-depth: 0
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: '22'
28+
29+
- name: Install Codex CLI
30+
run: npm i -g @openai/codex
31+
32+
- name: Codex login
33+
env:
34+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
35+
run: |
36+
set -euo pipefail
37+
echo "$OPENAI_API_KEY" | codex login --with-api-key
38+
39+
- name: Compute merge-base diff (compact)
40+
run: |
41+
set -euo pipefail
42+
BASE_REF='${{ github.event.pull_request.base.ref }}'
43+
git fetch --no-tags origin "$BASE_REF":"refs/remotes/origin/$BASE_REF"
44+
MB=$(git merge-base "origin/$BASE_REF" HEAD)
45+
git diff --unified=0 "$MB"..HEAD > pr.diff
46+
git --no-pager diff --stat "$MB"..HEAD > pr.stat || true
47+
48+
- name: Build prompt and run Codex (guard + fallback)
49+
env:
50+
PR_URL: ${{ github.event.pull_request.html_url }}
51+
PR_NUMBER: ${{ github.event.pull_request.number }}
52+
run: |
53+
set -euo pipefail
54+
MAX=${MAX_DIFF_BYTES:-900000} # ~0.9MB ceiling; override via env if needed
55+
56+
BYTES=$(wc -c < pr.diff || echo 0)
57+
echo "pr.diff size: $BYTES bytes (limit: $MAX)"
58+
59+
# Common prelude for AppSec review
60+
{
61+
echo "You are a skilled AppSec reviewer. Analyze this PR for:"
62+
echo "bugs, vulnerabilities, loss of funds issues, crypto attack vectors, signature vulnerability, replay attacks etc.."
63+
echo "Think deeply. Prioritize the *changed hunks* in pr.diff, but open any other files"
64+
echo "in the checkout as needed for context."
65+
echo
66+
echo "Return a tight executive summary, then bullets with:"
67+
echo "- severity (high/med/low)"
68+
echo "- file:line pointers"
69+
echo "- concrete fixes & example patches"
70+
echo '- if N/A, say "No significant issues found."'
71+
echo
72+
echo "PR URL: $PR_URL"
73+
echo
74+
echo "Formatting requirements:"
75+
echo "- Output MUST be GitHub-flavored Markdown (GFM)."
76+
echo "- Start with '## Executive summary' (one short paragraph)."
77+
echo "- Then '## Findings and fixes' as a bullet list."
78+
echo "- Use fenced code blocks for patches/configs with language tags (diff, yaml, etc.)."
79+
echo "- Use inline code for file:line and identifiers."
80+
} > prompt.txt
81+
82+
if [ "$BYTES" -le "$MAX" ] && [ "$BYTES" -gt 0 ]; then
83+
echo "Using embedded diff path (<= $MAX bytes)"
84+
{
85+
echo "Unified diff (merge-base vs HEAD):"
86+
echo '```diff'
87+
cat pr.diff
88+
echo '```'
89+
} >> prompt.txt
90+
91+
echo "---- prompt head ----"; head -n 40 prompt.txt >&2
92+
echo "---- prompt size ----"; wc -c prompt.txt >&2
93+
94+
# Run Codex with a scrubbed env: only OPENAI_API_KEY, PATH, HOME
95+
env -i OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" PATH="$PATH" HOME="$HOME" \
96+
codex --model gpt-5-codex --ask-for-approval never exec \
97+
--sandbox read-only \
98+
--output-last-message review.md \
99+
< prompt.txt \
100+
> codex.log 2>&1
101+
102+
else
103+
echo "Large diff – switching to fallback that lets Codex fetch the .diff URL"
104+
# Recompute merge-base and HEAD for clarity in the prompt
105+
BASE_REF='${{ github.event.pull_request.base.ref }}'
106+
git fetch --no-tags origin "$BASE_REF":"refs/remotes/origin/$BASE_REF"
107+
MB=$(git merge-base "origin/$BASE_REF" HEAD)
108+
HEAD_SHA=$(git rev-parse HEAD)
109+
DIFF_URL="${PR_URL}.diff"
110+
111+
{
112+
echo "The diff is too large to embed safely in this CI run."
113+
echo "Please fetch and analyze the diff from this URL:"
114+
echo "$DIFF_URL"
115+
echo
116+
echo "Commit range (merge-base...HEAD):"
117+
echo "merge-base: $MB"
118+
echo "head: $HEAD_SHA"
119+
echo
120+
echo "For quick orientation, here is the diffstat:"
121+
echo '```'
122+
cat pr.stat || true
123+
echo '```'
124+
echo
125+
echo "After fetching the diff, continue with the same review instructions above."
126+
} >> prompt.txt
127+
128+
echo "---- fallback prompt head ----"; head -n 80 prompt.txt >&2
129+
echo "---- fallback prompt size ----"; wc -c prompt.txt >&2
130+
131+
# Network-enabled only for this large-diff case; still scrub env
132+
env -i OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" PATH="$PATH" HOME="$HOME" \
133+
codex --model gpt-5-codex --ask-for-approval never exec \
134+
--sandbox danger-full-access \
135+
--output-last-message review.md \
136+
< prompt.txt \
137+
> codex.log 2>&1
138+
fi
139+
140+
# Defensive: ensure later steps don't explode
141+
if [ ! -s review.md ]; then
142+
echo "_Codex produced no output._" > review.md
143+
fi
144+
145+
- name: Post parent message in Slack (blocks)
146+
id: post_parent
147+
env:
148+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
149+
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
150+
run: |
151+
resp=$(curl -s -X POST https://slack.com/api/chat.postMessage \
152+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
153+
-H 'Content-type: application/json; charset=utf-8' \
154+
--data "$(jq -n \
155+
--arg ch "$SLACK_CHANNEL_ID" \
156+
--arg n "${{ github.event.pull_request.number }}" \
157+
--arg t "${{ github.event.pull_request.title }}" \
158+
--arg a "${{ github.event.pull_request.user.login }}" \
159+
--arg u "${{ github.event.pull_request.html_url }}" \
160+
'{
161+
channel: $ch,
162+
text: ("PR #" + $n + ": " + $t),
163+
blocks: [
164+
{ "type":"section", "text":{"type":"mrkdwn","text":("*PR #"+$n+":* "+$t)} },
165+
{ "type":"section", "text":{"type":"mrkdwn","text":("• Author: "+$a)} },
166+
{ "type":"section", "text":{"type":"mrkdwn","text":("• Link: <"+$u+">")} }
167+
],
168+
unfurl_links:false, unfurl_media:false
169+
}')" )
170+
echo "ts=$(echo "$resp" | jq -r '.ts')" >> "$GITHUB_OUTPUT"
171+
172+
- name: Thread reply with review (upload via Slack external upload API)
173+
env:
174+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
175+
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
176+
TS: ${{ steps.post_parent.outputs.ts }}
177+
run: |
178+
set -euo pipefail
179+
180+
# robust byte count (works on Linux & macOS)
181+
BYTES=$( (stat -c%s review.md 2>/dev/null || stat -f%z review.md 2>/dev/null) )
182+
BYTES=${BYTES:-$(wc -c < review.md | tr -d '[:space:]')}
183+
184+
ticket=$(curl -sS -X POST https://slack.com/api/files.getUploadURLExternal \
185+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
186+
-H "Content-type: application/x-www-form-urlencoded" \
187+
--data-urlencode "filename=codex_review.md" \
188+
--data "length=$BYTES" \
189+
--data "snippet_type=markdown")
190+
echo "$ticket"
191+
upload_url=$(echo "$ticket" | jq -r '.upload_url')
192+
file_id=$(echo "$ticket" | jq -r '.file_id')
193+
test "$upload_url" != "null" -a "$file_id" != "null" || { echo "getUploadURLExternal failed: $ticket" >&2; exit 1; }
194+
195+
curl -sS -X POST "$upload_url" \
196+
-F "filename=@review.md;type=text/markdown" \
197+
> /dev/null
198+
199+
payload=$(jq -n --arg fid "$file_id" --arg ch "$SLACK_CHANNEL_ID" --arg ts "$TS" \
200+
--arg title "Codex Security Review" --arg ic "Automated Codex review attached." \
201+
'{files:[{id:$fid, title:$title}], channel_id:$ch, thread_ts:$ts, initial_comment:$ic}')
202+
resp=$(curl -sS -X POST https://slack.com/api/files.completeUploadExternal \
203+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
204+
-H "Content-type: application/json; charset=utf-8" \
205+
--data "$payload")
206+
echo "$resp"
207+
test "$(echo "$resp" | jq -r '.ok')" = "true" || { echo "files.completeUploadExternal failed: $resp" >&2; exit 1; }

0 commit comments

Comments
 (0)