2323 environment : n8n-sending
2424 outputs :
2525 filtered_file_count : ${{ steps.filter_files.outputs.file_count }}
26+ should_send : ${{ steps.filter_files.outputs.file_count != '0' && steps.diff_threshold.outputs.skip_send != 'true' && steps.send_payload.outputs.cancelled != 'true' && steps.send_payload.outputs.error != 'true' }}
27+ diff_char_count : ${{ steps.diff_threshold.outputs.diff_char_count }}
28+ pr_number : ${{ steps.run_context.outputs.pr_number }}
29+ repository : ${{ steps.run_context.outputs.repository }}
30+ run_url : ${{ steps.run_context.outputs.run_url }}
31+ response_payload : ${{ steps.capture_payload.outputs.payload }}
32+ cancelled : ${{ steps.send_payload.outputs.cancelled }}
33+ send_executed : ${{ steps.send_payload.outputs.executed }}
34+ send_error : ${{ steps.send_payload.outputs.error }}
2635 env :
2736 PR_NUMBER : ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
2837 IS_MANUAL_RUN : ${{ github.event_name == 'workflow_dispatch' }}
38+ MIN_DIFF_CHAR_THRESHOLD : ${{ vars.N8N_MIN_DIFF_CHAR_THRESHOLD || '1000' }}
2939 steps :
3040 # Step 1: Checkout repository
3141 - name : Checkout repository
3646 id : uuid
3747 run : echo "run_token=$(uuidgen)" >> "$GITHUB_OUTPUT"
3848
39- # Step 2b: Require dispatcher to have write/maintain/admin access
49+ - name : Capture run context
50+ id : run_context
51+ run : |
52+ echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
53+ echo "repository=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
54+ echo "run_url=${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT"
55+
56+ # Step 2b: Require dispatcher to have access
4057 - name : Validate dispatcher permissions
4158 if : ${{ github.event_name == 'workflow_dispatch' }}
4259 env :
@@ -213,7 +230,16 @@ jobs:
213230 run : |
214231 gzip -c pr.diff > pr.diff.gz
215232 base64 -w 0 pr.diff.gz > diff.b64
216- echo "::add-mask::$(cat diff.b64)"
233+ python3 - <<'PY'
234+ from pathlib import Path
235+
236+ chunk_size = 8000
237+ diff_contents = Path("diff.b64").read_text()
238+ for start in range(0, len(diff_contents), chunk_size):
239+ chunk = diff_contents[start:start + chunk_size]
240+ if chunk:
241+ print(f"::add-mask::{chunk}")
242+ PY
217243
218244 # Step 9: Inspect payload sizes for debugging
219245 - name : Debug payload size
@@ -223,38 +249,97 @@ jobs:
223249 echo "Commits metadata size: $(stat -c%s commits.json) bytes"
224250 echo "Compressed diff size: $(stat -c%s pr.diff.gz) bytes"
225251
252+ # Step 9b: Evaluate diff length against threshold
253+ - name : Evaluate diff char threshold
254+ id : diff_threshold
255+ run : |
256+ set -euo pipefail
257+
258+ THRESHOLD="${MIN_DIFF_CHAR_THRESHOLD}"
259+ if [[ ! "$THRESHOLD" =~ ^[0-9]+$ ]]; then
260+ echo "Invalid MIN_DIFF_CHAR_THRESHOLD='$THRESHOLD'; defaulting to 0"
261+ THRESHOLD=0
262+ fi
263+
264+ CHAR_COUNT=$(wc -c < pr.diff | tr -d '[:space:]')
265+ echo "diff_char_count=${CHAR_COUNT}" >> "$GITHUB_OUTPUT"
266+ echo "threshold=${THRESHOLD}" >> "$GITHUB_OUTPUT"
267+
268+ if (( CHAR_COUNT < THRESHOLD )); then
269+ echo "skip_send=true" >> "$GITHUB_OUTPUT"
270+ echo "Filtered diff char count ${CHAR_COUNT} is below threshold ${THRESHOLD}; skipping downstream send."
271+ else
272+ echo "skip_send=false" >> "$GITHUB_OUTPUT"
273+ echo "Filtered diff char count ${CHAR_COUNT} meets threshold ${THRESHOLD}; continuing."
274+ fi
275+
276+ # Step 9c: Short-circuit when below threshold
277+ - name : Diff below threshold
278+ if : ${{ steps.filter_files.outputs.file_count != '0' && steps.diff_threshold.outputs.skip_send == 'true' }}
279+ run : |
280+ echo "ℹ️ Diff char count (${{ steps.diff_threshold.outputs.diff_char_count }}) is below threshold (${{ steps.diff_threshold.outputs.threshold }}). Skipping n8n send."
281+
226282 # Step 10: Send consolidated payload to n8n
227283 - name : Combine and send to n8n webhook
228- if : ${{ steps.filter_files.outputs.file_count != '0' }}
284+ id : send_payload
285+ if : ${{ steps.filter_files.outputs.file_count != '0' && steps.diff_threshold.outputs.skip_send != 'true' }}
229286 env :
230287 N8N_WEBHOOK_URL : ${{ secrets.N8N_WEBHOOK_URL }}
231288 N8N_SENDING_TOKEN : ${{ secrets.N8N_SENDING_TOKEN }}
289+ RUN_TOKEN : ${{ steps.uuid.outputs.run_token }}
232290 run : |
233291 set -euo pipefail
234292
235- jq -n \
236- --slurpfile pr pr.json \
237- --slurpfile files files.json \
238- --slurpfile commits commits.json \
239- --rawfile diff_base64 diff.b64 \
240- --arg run_token "${{ steps.uuid.outputs.run_token }}" \
241- --arg n8n_sending_token "$N8N_SENDING_TOKEN" \
242- '{
243- pr: $pr[0],
244- files: $files[0],
245- commits: $commits[0],
246- diff_base64: ($diff_base64 | gsub("\n"; "")),
247- token: $run_token,
248- n8n_sending_token: $n8n_sending_token
249- }' > payload.json
293+ echo "cancelled=false" >> "$GITHUB_OUTPUT"
294+ echo "executed=true" >> "$GITHUB_OUTPUT"
295+ echo "error=false" >> "$GITHUB_OUTPUT"
296+
297+ python3 - <<'PY'
298+ import json
299+ import os
300+ from pathlib import Path
301+
302+ root = Path(".")
303+
304+ def read_json(filename):
305+ path = root / filename
306+ try:
307+ return json.loads(path.read_text())
308+ except FileNotFoundError:
309+ raise SystemExit(f"{filename} not found")
310+ except json.JSONDecodeError as exc:
311+ raise SystemExit(f"Unable to parse {filename}: {exc}")
312+
313+ pr = read_json("pr.json")
314+ files = read_json("files.json")
315+ commits = read_json("commits.json")
316+
317+ diff_path = root / "diff.b64"
318+ try:
319+ diff_base64 = diff_path.read_text().replace("\n", "")
320+ except FileNotFoundError:
321+ raise SystemExit("diff.b64 not found")
322+
323+ payload = {
324+ "pr": pr,
325+ "files": files,
326+ "commits": commits,
327+ "diff_base64": diff_base64,
328+ "token": os.environ.get("RUN_TOKEN", ""),
329+ "n8n_sending_token": os.environ.get("N8N_SENDING_TOKEN", "")
330+ }
331+
332+ (root / "payload.json").write_text(json.dumps(payload))
333+ PY
250334
251335 echo "::add-mask::$N8N_SENDING_TOKEN"
252336
253337 PAYLOAD_SIZE=$(stat -c%s payload.json)
254338 MAX_BYTES=$((10*1024*1024))
255339 if (( PAYLOAD_SIZE > MAX_BYTES )); then
256- echo "Payload too large ($PAYLOAD_SIZE bytes). Aborting send."
257- exit 1
340+ echo "Payload too large ($PAYLOAD_SIZE bytes)."
341+ echo "error=true" >> "$GITHUB_OUTPUT"
342+ exit 0
258343 fi
259344
260345 RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
@@ -271,46 +356,66 @@ jobs:
271356 STATUS=$(jq -r ".status" response_body.json)
272357 MATCHED=$(jq -r ".token" response_body.json)
273358 if [ "$MATCHED" != "${{ steps.uuid.outputs.run_token }}" ] || [ "$STATUS" != "completed" ]; then
359+ if [ "$STATUS" = "cancelled" ]; then
360+ echo "n8n workflow reported cancellation; skipping downstream processing."
361+ echo "cancelled=true" >> "$GITHUB_OUTPUT"
362+ exit 0
363+ fi
274364 echo "n8n workflow failed or token mismatch"
275- exit 1
365+ echo "error=true" >> "$GITHUB_OUTPUT"
366+ exit 0
276367 fi
277368
278369 if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
279370 echo "n8n workflow failed (HTTP $HTTP_STATUS)"
280- exit 1
371+ echo "error=true" >> "$GITHUB_OUTPUT"
372+ exit 0
373+ fi
374+
375+ - name : Capture response payload
376+ id : capture_payload
377+ if : ${{ steps.filter_files.outputs.file_count != '0' && steps.diff_threshold.outputs.skip_send != 'true' && steps.send_payload.outputs.cancelled != 'true' && steps.send_payload.outputs.error != 'true' }}
378+ run : |
379+ set -euo pipefail
380+ if [ ! -s response_body.json ]; then
381+ echo "payload=" >> "$GITHUB_OUTPUT"
382+ exit 0
281383 fi
384+ base64 -w0 response_body.json > response_body.b64
385+ echo "payload=$(cat response_body.b64)" >> "$GITHUB_OUTPUT"
386+ rm -f response_body.b64
387+
388+ - name : Enforce n8n send failure
389+ if : ${{ steps.send_payload.outputs.error == 'true' }}
390+ run : |
391+ echo "n8n send step reported failure. Marking job as failed."
392+ exit 1
282393
283394 # Step 11: Short-circuit when nothing qualifies
284395 - name : No eligible files to send
285396 if : ${{ steps.filter_files.outputs.file_count == '0' }}
286397 run : echo "ℹ️ No eligible files after filtering llms/.ai paths. Skipping n8n send."
287398
288- # Step 12: Archive n8n response for next job
289- - name : Upload n8n response artifact
290- if : ${{ steps.filter_files.outputs.file_count != '0' }}
291- uses : actions/upload-artifact@v4
292- with :
293- name : n8n-response
294- path : response_body.json
295- if-no-files-found : error
296- retention-days : 1
297-
298399 receive-validate-and-comment :
299400 runs-on : ubuntu-latest
300401 needs : gather-and-send
301- if : ${{ needs.gather-and-send.result == 'success' && needs.gather-and-send.outputs.filtered_file_count != '0' }}
402+ if : ${{ needs.gather-and-send.result == 'success' && needs.gather-and-send.outputs.filtered_file_count != '0' && needs.gather-and-send.outputs.should_send == 'true' }}
302403 environment : n8n-receiving
303404 steps :
304405 # Step 13: Re-checkout repo (fresh workspace)
305406 - name : Checkout repository
306407 uses : actions/checkout@v4
307408
308- # Step 14: Retrieve n8n response artifact
309- - name : Download n8n response
310- uses : actions/download-artifact@v4
311- with :
312- name : n8n-response
313- path : .
409+ # Step 14: Restore response payload from upstream output
410+ - name : Restore response payload
411+ run : |
412+ set -euo pipefail
413+ payload='${{ needs.gather-and-send.outputs.response_payload }}'
414+ if [ -z "$payload" ]; then
415+ echo "No response payload found; exiting."
416+ exit 1
417+ fi
418+ printf '%s' "$payload" | base64 -d > response_body.json
314419
315420 # Step 15: Confirm handshake token
316421 - name : Validate receiving token
@@ -545,3 +650,74 @@ jobs:
545650 done
546651
547652 echo "✅ Done posting verification reviews."
653+
654+ # Step 18b: Clean up response artifacts
655+ - name : Remove response payload
656+ if : ${{ always() }}
657+ run : |
658+ rm -f response_body.json review_batches.json || true
659+
660+ notify-on-failure :
661+ runs-on : ubuntu-latest
662+ needs :
663+ - gather-and-send
664+ - receive-validate-and-comment
665+ if : ${{ always() && ((needs.gather-and-send.result == 'failure' && (needs.gather-and-send.outputs.send_executed != 'true' || needs.gather-and-send.outputs.send_error == 'true')) || needs.receive-validate-and-comment.result == 'failure') }}
666+ env :
667+ GATHER_RESULT : ${{ needs.gather-and-send.result }}
668+ RECEIVE_RESULT : ${{ needs.receive-validate-and-comment.result }}
669+ PR_NUMBER : ${{ needs.gather-and-send.outputs.pr_number }}
670+ REPOSITORY : ${{ needs.gather-and-send.outputs.repository || github.repository }}
671+ RUN_URL : ${{ needs.gather-and-send.outputs.run_url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}
672+ WORKFLOW_NAME : ${{ github.workflow }}
673+ RUN_ID : ${{ github.run_id }}
674+ RUN_ATTEMPT : ${{ github.run_attempt }}
675+ ERROR_WEBHOOK_URL : ${{ secrets.ERROR_WEBHOOK_URL }}
676+ ACTOR : ${{ github.actor }}
677+ SEND_EXECUTED : ${{ needs.gather-and-send.outputs.send_executed }}
678+ SEND_ERROR : ${{ needs.gather-and-send.outputs.send_error }}
679+ steps :
680+ - name : Evaluate failure state
681+ id : alert
682+ run : |
683+ set -euo pipefail
684+ gather="${GATHER_RESULT:-unknown}"
685+ receive="${RECEIVE_RESULT:-skipped}"
686+ if [[ "$gather" == "failure" || "$receive" == "failure" ]]; then
687+ echo "alert=true" >> "$GITHUB_OUTPUT"
688+ echo "Failure detected (gather=$gather, receive=$receive)."
689+ else
690+ echo "alert=false" >> "$GITHUB_OUTPUT"
691+ echo "No downstream alert required (gather=$gather, receive=$receive)."
692+ fi
693+
694+ - name : Send failure webhook
695+ if : ${{ steps.alert.outputs.alert == 'true' }}
696+ run : |
697+ set -euo pipefail
698+
699+ if [[ -z "${ERROR_WEBHOOK_URL:-}" ]]; then
700+ echo "Webhook URL secret not configured; skipping alert dispatch."
701+ exit 0
702+ fi
703+
704+ gather="${GATHER_RESULT:-unknown}"
705+ receive="${RECEIVE_RESULT:-skipped}"
706+ pr="${PR_NUMBER:-unknown}"
707+ repo="${REPOSITORY:-${{ github.repository }}}"
708+ payload=$(jq -n \
709+ --arg repo "$repo" \
710+ --arg pr "$pr" \
711+ --arg gather_status "$gather" \
712+ --arg receive_status "$receive" \
713+ --arg run_url "$RUN_URL" \
714+ --arg workflow "$WORKFLOW_NAME" \
715+ --arg run_id "$RUN_ID" \
716+ --arg run_attempt "$RUN_ATTEMPT" \
717+ --arg actor "$ACTOR" \
718+ '{repository:$repo, pr_number:$pr, gather_status:$gather_status, receive_status:$receive_status, run_url:$run_url, workflow:$workflow, run_id:$run_id, run_attempt:$run_attempt, actor:$actor}' )
719+
720+ curl -sS -X POST \
721+ -H "Content-Type: application/json" \
722+ --data "$payload" \
723+ "$ERROR_WEBHOOK_URL"
0 commit comments