1+ name : Trigger n8n Webhook with Complete PR Info
2+
3+ on :
4+ pull_request :
5+ types : [opened, reopened]
6+ workflow_dispatch : {}
7+
8+ permissions :
9+ contents : write
10+ pull-requests : write
11+
12+ jobs :
13+ gather-and-send :
14+ runs-on : ubuntu-latest
15+ environment : n8n-sending
16+ steps :
17+
18+ # 1. Checkout repository
19+
20+ - name : Checkout repository
21+ uses : actions/checkout@v4
22+
23+ # 2. Generate Run UUID
24+
25+ - name : Generate Run UUID
26+ id : uuid
27+ run : echo "run_token=$(uuidgen)" >> $GITHUB_OUTPUT
28+
29+
30+ # 3. Pre-flight checks
31+
32+ - name : Validate setup
33+ run : |
34+ if [[ -z "${{ secrets.GITHUB_TOKEN }}" ]]; then
35+ echo "Missing GITHUB_TOKEN secret."
36+ exit 1
37+ fi
38+
39+ if [[ -z "${{ github.event.pull_request.number }}" ]]; then
40+ echo "No PR number found in event payload."
41+ exit 1
42+ fi
43+
44+ if [[ -z "${{ secrets.N8N_SENDING_TOKEN }}" ]]; then
45+ echo "Missing N8N_SENDING_TOKEN secret."
46+ exit 1
47+ fi
48+
49+ # 4-6. Fetch PR metadata, files, commits
50+
51+ - name : Fetch PR metadata
52+ run : |
53+ gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }} > pr.json
54+ env :
55+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
56+
57+ - name : Fetch PR files
58+ run : |
59+ gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files > files.json
60+ env :
61+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
62+
63+ - name : Fetch PR commits
64+ run : |
65+ gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/commits > commits.json
66+ env :
67+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
68+
69+ # 7. Fetch PR diff and compress
70+
71+ - name : Fetch PR diff and compress
72+ run : |
73+ curl -sSL \
74+ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
75+ -H "Accept: application/vnd.github.v3.diff" \
76+ "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \
77+ > pr.diff
78+
79+ gzip -c pr.diff > pr.diff.gz
80+ base64 -w 0 pr.diff.gz > diff.b64
81+ echo "::add-mask::$(cat diff.b64)"
82+
83+ # 8. Debug payload size
84+
85+ - name : Debug payload size
86+ run : |
87+ echo "PR metadata size: $(stat -c%s pr.json) bytes"
88+ echo "Files metadata size: $(stat -c%s files.json) bytes"
89+ echo "Commits metadata size: $(stat -c%s commits.json) bytes"
90+ echo "Compressed diff size: $(stat -c%s pr.diff.gz) bytes"
91+
92+ # 8b. Remove llms-related files before payload
93+
94+ - name : Remove llms-related files
95+ run : |
96+ jq 'map(select(.filename | test("llms"; "i") | not))' files.json > cleaned.json
97+ mv cleaned.json files.json
98+
99+ # 9. Combine and send payload to n8n
100+
101+ - name : Combine and send to n8n webhook
102+ env :
103+ N8N_WEBHOOK_URL : ${{ secrets.N8N_WEBHOOK_URL }}
104+ N8N_SENDING_TOKEN : ${{ secrets.N8N_SENDING_TOKEN }}
105+ run : |
106+ set -e
107+
108+ jq -n \
109+ --slurpfile pr pr.json \
110+ --slurpfile files files.json \
111+ --slurpfile commits commits.json \
112+ --arg diff_base64 "$(cat diff.b64)" \
113+ --arg run_token "${{ steps.uuid.outputs.run_token }}" \
114+ --arg n8n_sending_token "$N8N_SENDING_TOKEN" \
115+ '{
116+ pr: $pr[0],
117+ files: $files[0],
118+ commits: $commits[0],
119+ diff_base64: $diff_base64,
120+ token: $run_token,
121+ n8n_sending_token: $n8n_sending_token
122+ }' > payload.json
123+
124+ echo "::add-mask::$N8N_SENDING_TOKEN"
125+
126+ PAYLOAD_SIZE=$(stat -c%s payload.json)
127+ MAX_BYTES=$((10*1024*1024))
128+ if (( PAYLOAD_SIZE > MAX_BYTES )); then
129+ echo "Payload too large ($PAYLOAD_SIZE bytes). Aborting send."
130+ exit 1
131+ fi
132+
133+ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
134+ -H "Content-Type: application/json" \
135+ --data-binary @payload.json \
136+ "$N8N_WEBHOOK_URL")
137+
138+ HTTP_BODY=$(echo "$RESPONSE" | sed '$d')
139+ HTTP_STATUS=$(echo "$RESPONSE" | tail -n1)
140+
141+ echo "n8n responded with status: $HTTP_STATUS"
142+ echo "$HTTP_BODY" > response_body.json
143+
144+ STATUS=$(jq -r ".status" response_body.json)
145+ MATCHED=$(jq -r ".token" response_body.json)
146+ if [ "$MATCHED" != "${{ steps.uuid.outputs.run_token }}" ] || [ "$STATUS" != "completed" ]; then
147+ echo "n8n workflow failed or token mismatch"
148+ exit 1
149+ fi
150+
151+ if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
152+ echo "n8n workflow failed (HTTP $HTTP_STATUS)"
153+ exit 1
154+ fi
155+
156+ # 9b. Upload n8n response artifact
157+
158+ - name : Upload n8n response artifact
159+ uses : actions/upload-artifact@v4
160+ with :
161+ name : n8n-response
162+ path : response_body.json
163+ if-no-files-found : error
164+ retention-days : 1
165+
166+
167+ # Step 10 moved to a separate job under n8n-receiving env
168+
169+ receive-validate-and-comment :
170+ runs-on : ubuntu-latest
171+ needs : gather-and-send
172+ environment : n8n-receiving
173+ steps :
174+ # 1. Checkout repository
175+ - name : Checkout repository
176+ uses : actions/checkout@v4
177+
178+ # 2. Download n8n response
179+ - name : Download n8n response
180+ uses : actions/download-artifact@v4
181+ with :
182+ name : n8n-response
183+ path : .
184+
185+
186+ # 3. Validate receiving token
187+ - name : Validate receiving token
188+ env :
189+ EXPECTED_TOKEN : ${{ secrets.N8N_RECEIVING_TOKEN }}
190+ run : |
191+ RECEIVED_TOKEN=$(jq -r 'if type=="array" then .[0].receiving_token else .receiving_token end // empty' response_body.json)
192+ if [ -z "$RECEIVED_TOKEN" ]; then
193+ echo "No receiving_token provided by n8n"
194+ exit 1
195+ fi
196+ if [ "$RECEIVED_TOKEN" != "$EXPECTED_TOKEN" ]; then
197+ echo "Receiving token mismatch"
198+ exit 1
199+ fi
200+ echo "✅ Receiving token validated successfully"
201+
202+ # 4. Post inline code suggestions (robust; supports deletions)
203+ - name : Post inline suggestions via API (final)
204+ env :
205+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
206+ run : |
207+ set -euo pipefail
208+
209+ RESPONSE_BODY=$(cat response_body.json)
210+ OBJ=$(echo "$RESPONSE_BODY" | jq -c 'if type=="array" then .[0] else . end')
211+
212+ STATUS=$(echo "$OBJ" | jq -r '.status // empty')
213+ if [ "$STATUS" != "completed" ]; then
214+ echo "n8n output not completed (status='$STATUS')"
215+ jq . response_body.json || true
216+ exit 1
217+ fi
218+
219+ OWNER=$(echo "$OBJ" | jq -r '.repo_owner // empty')
220+ REPO=$(echo "$OBJ" | jq -r '.repo_name // empty')
221+ PR=$(echo "$OBJ" | jq -r '.pr_number // empty')
222+ if [ -z "$OWNER" ] || [ -z "$REPO" ] || [ -z "$PR" ]; then
223+ echo "Missing repo context (owner='$OWNER' repo='$REPO' pr='$PR')"
224+ jq . response_body.json || true
225+ exit 1
226+ fi
227+
228+ echo "Posting inline suggestions to $OWNER/$REPO#${PR}"
229+
230+ posted=0
231+ echo "$OBJ" | jq -c '.prepared_comment_payloads // [] | .[]' | while read -r PAYLOAD; do
232+ # Ensure commit_id exists (avoid 422)
233+ CID=$(echo "$PAYLOAD" | jq -r '.commit_id // empty')
234+ if [ -z "$CID" ]; then
235+ echo "⚠️ Skipping payload without commit_id"
236+ continue
237+ fi
238+
239+ # If body is empty/whitespace, replace with an empty suggestion block (deletion)
240+ BODY_TXT=$(echo "$PAYLOAD" | jq -r '.body // ""')
241+ if [ -z "${BODY_TXT//[$'\t\r\n ']}" ]; then
242+ PAYLOAD=$(echo "$PAYLOAD" | jq --rawfile emptySug <(printf '```suggestion\n```\n') '.body = $emptySug')
243+ fi
244+
245+ RESP=$(curl -sS -w "%{http_code}" -X POST \
246+ -H "Authorization: Bearer ${GH_TOKEN}" \
247+ -H "Accept: application/vnd.github.v3+json" \
248+ "https://api.github.com/repos/${OWNER}/${REPO}/pulls/${PR}/comments" \
249+ -d "$PAYLOAD")
250+
251+ CODE="${RESP:(-3)}"
252+ BODY="${RESP%$CODE}"
253+
254+ if [[ "$CODE" -lt 200 || "$CODE" -ge 300 ]]; then
255+ echo "❌ Failed to post suggestion (HTTP $CODE)"
256+ echo "$BODY"
257+ exit 1
258+ fi
259+ posted=$((posted+1))
260+ echo "✅ Posted suggestion #$posted"
261+ done
262+
263+ echo "—— Skipped upstream (for visibility) ——"
264+ echo "$OBJ" | jq -r '.skipped_comments // [] | .[] | "\(.file_path // "-")#L\(.line_number // "-"): \(.reason // "-")"'
265+
266+ echo "✅ Done. Suggestions posted."
267+
268+ - name : Post verification comments
269+ env :
270+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
271+ run : |
272+ set -euo pipefail
273+
274+ RESPONSE_BODY=$(cat response_body.json)
275+
276+ # Extract verification review markdown robustly from many shapes
277+ VERIFS=$(
278+ echo "$RESPONSE_BODY" | jq -c '
279+ (if type=="array" then .[0] else . end) as $o
280+ | def to_arr(x):
281+ if (x|type) == "array" then x
282+ elif (x|type) == "object" then [x]
283+ elif (x|type) == "string" then (try (x|fromjson) catch [])
284+ else [] end;
285+
286+ [
287+ # Common direct locations
288+ (to_arr($o.reviews) // []) as $r1
289+ | ($r1 | map(select(((.type? | tostring | ascii_downcase) == "verification")))
290+ | map(.formattedReview // .review // .body // .comment // .text // .content // "")),
291+ (to_arr($o.review) // []) as $r2
292+ | ($r2 | map(select(((.type? | tostring | ascii_downcase) == "verification")))
293+ | map(.formattedReview // .review // .body // .comment // .text // .content // "")),
294+
295+ # Anywhere in the object tree: find objects with type=verification
296+ ($o | .. | objects
297+ | select(((.type? | tostring | ascii_downcase) == "verification"))
298+ | (.formattedReview // .review // .body // .comment // .text // .content // ""))
299+
300+ ]
301+ | flatten
302+ | map(select(type == "string" and (.|length) > 0))
303+ | unique
304+ '
305+ )
306+
307+ COUNT=$(echo "$VERIFS" | jq 'length')
308+ if [ "$COUNT" -eq 0 ]; then
309+ echo "No verification reviews found; skipping."
310+ exit 0
311+ fi
312+
313+ # Extract repo context (robust to array/object root)
314+ OWNER=$(echo "$RESPONSE_BODY" | jq -r 'if type=="array" then .[0].repo_owner else .repo_owner end // empty')
315+ REPO=$(echo "$RESPONSE_BODY" | jq -r 'if type=="array" then .[0].repo_name else .repo_name end // empty')
316+ PR=$(echo "$RESPONSE_BODY" | jq -r 'if type=="array" then .[0].pr_number else .pr_number end // empty')
317+
318+ if [ -z "$OWNER" ] || [ -z "$REPO" ] || [ -z "$PR" ]; then
319+ echo "Missing repo context; skipping verification comments."
320+ exit 0
321+ fi
322+
323+ echo "Found $COUNT verification review(s). Preview:"
324+ echo "$VERIFS" | jq -r 'to_entries[] | "\(.key): " + (.value | .[0:160] + (if length>160 then "…" else "" end))'
325+
326+ # Post each block as a PR comment (not inline)
327+ echo "$VERIFS" | jq -r '.[] + "\u0000"' | while IFS= read -r -d '' BODY; do
328+ # Skip truly empty
329+ if [ -z "${BODY//[$'\t\r\n ']}" ]; then
330+ echo "Skipping empty verification body"
331+ continue
332+ fi
333+
334+ RESP=$(curl -sS -w "%{http_code}" -X POST \
335+ -H "Authorization: Bearer ${GH_TOKEN}" \
336+ -H "Accept: application/vnd.github.v3+json" \
337+ "https://api.github.com/repos/${OWNER}/${REPO}/issues/${PR}/comments" \
338+ -d "$(jq -nc --arg b "$BODY" '{body:$b}')")
339+
340+ CODE="${RESP:(-3)}"
341+ RBODY="${RESP%$CODE}"
342+
343+ if [[ "$CODE" -lt 200 || "$CODE" -ge 300 ]]; then
344+ echo "❌ Failed to post verification comment (HTTP $CODE)"
345+ echo "$RBODY"
346+ exit 1
347+ fi
348+
349+ url=$(echo "$RBODY" | jq -r '.html_url // empty')
350+ echo "✅ Posted verification review ${url:+→ $url}"
351+ done
352+
353+ echo "✅ Done posting verification reviews."
354+
355+
0 commit comments