Skip to content

Commit 95393d8

Browse files
authored
add n8n script (#1100)
* add n8n script * test * test * revert --------- Co-authored-by: Taylor Lucero <[email protected]>
1 parent 092e44e commit 95393d8

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
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

Comments
 (0)