mirror of
https://github.com/Sea-Haven-Industries/shoc-pr-review-runner.git
synced 2026-09-30 05:53:12 +00:00
123 lines
4.9 KiB
Bash
123 lines
4.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
# Collect the PR diff and changed-file contents for the review prompt.
|
|||
|
|
#
|
|||
|
|
# Runs in the gates job while the App token is still valid, and BEFORE any
|
|||
|
|
# PR-authored code executes, so the collected context cannot be tampered with by
|
|||
|
|
# the build. File contents are read from git objects (`git cat-file`), never
|
|||
|
|
# from the filesystem: a PR can add a symlink pointing outside the checkout, and
|
|||
|
|
# a filesystem read would follow it and leak host files into the prompt and the
|
|||
|
|
# uploaded artifacts. Reading blobs cannot escape the tree.
|
|||
|
|
#
|
|||
|
|
# Reads: GH_TOKEN, REVIEW_TYPE, FRONTEND_REPO, BACKEND_REPO,
|
|||
|
|
# DIFF_MAX_BYTES / FILES_MAX_BYTES / FILE_MAX_BYTES
|
|||
|
|
# Writes: $ARTIFACTS_DIR/{prompt-diff.txt,prompt-files.txt,prompt-bounds.txt}
|
|||
|
|
|
|||
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||
|
|
# shellcheck source=lib.sh
|
|||
|
|
source "$SCRIPT_DIR/lib.sh"
|
|||
|
|
|
|||
|
|
require_env GH_TOKEN REVIEW_TYPE FRONTEND_REPO BACKEND_REPO
|
|||
|
|
DIFF_MAX_BYTES="${DIFF_MAX_BYTES:-200000}"
|
|||
|
|
FILES_MAX_BYTES="${FILES_MAX_BYTES:-120000}"
|
|||
|
|
FILE_MAX_BYTES="${FILE_MAX_BYTES:-65536}"
|
|||
|
|
|
|||
|
|
diff_section="$ARTIFACTS_DIR/prompt-diff.txt"
|
|||
|
|
files_section="$ARTIFACTS_DIR/prompt-files.txt"
|
|||
|
|
bounds_notes="$ARTIFACTS_DIR/prompt-bounds.txt"
|
|||
|
|
: >"$diff_section"; : >"$files_section"; : >"$bounds_notes"
|
|||
|
|
|
|||
|
|
side_in_scope() {
|
|||
|
|
case "$REVIEW_TYPE" in
|
|||
|
|
paired) return 0 ;;
|
|||
|
|
"$1") return 0 ;;
|
|||
|
|
*) return 1 ;;
|
|||
|
|
esac
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]'; }
|
|||
|
|
|
|||
|
|
# note_bound <text> — records a context-bound note. Untrusted path names are
|
|||
|
|
# sanitized (control characters removed, length capped) and marked, so a crafted
|
|||
|
|
# file name cannot forge lines in the evidence report.
|
|||
|
|
note_bound() {
|
|||
|
|
printf -- '- %s\n' "$(printf '%s' "$1" | tr -d '\000-\037' | cut -c1-300)" >>"$bounds_notes"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
collect_side() { # <side> <repo> <checkout-dir>
|
|||
|
|
local side="$1" repo="$2" dir="$3"
|
|||
|
|
local pr upper_side
|
|||
|
|
pr="$(jq -r '.pr' "$ARTIFACTS_DIR/$side-pr.json")"
|
|||
|
|
upper_side="$(upper "$side")"
|
|||
|
|
|
|||
|
|
local raw="$ARTIFACTS_DIR/$side.diff"
|
|||
|
|
gh api "repos/$repo/pulls/$pr" -H "Accept: application/vnd.github.diff" >"$raw" \
|
|||
|
|
|| die "failed to fetch diff for $repo#$pr"
|
|||
|
|
local size
|
|||
|
|
size="$(wc -c <"$raw" | tr -d ' ')"
|
|||
|
|
{
|
|||
|
|
printf '===== %s DIFF (%s #%s) =====\n' "$upper_side" "$repo" "$pr"
|
|||
|
|
if [ "$size" -gt "$DIFF_MAX_BYTES" ]; then
|
|||
|
|
head -c "$DIFF_MAX_BYTES" "$raw"
|
|||
|
|
printf '\n===== %s DIFF TRUNCATED: %s of %s bytes included =====\n' \
|
|||
|
|
"$upper_side" "$DIFF_MAX_BYTES" "$size"
|
|||
|
|
note_bound "$side diff truncated to $DIFF_MAX_BYTES of $size bytes"
|
|||
|
|
else
|
|||
|
|
cat "$raw"
|
|||
|
|
fi
|
|||
|
|
} >>"$diff_section"
|
|||
|
|
|
|||
|
|
# Changed files, enumerated NUL-delimited so newlines in path names cannot
|
|||
|
|
# split one record into several.
|
|||
|
|
local budget="$FILES_MAX_BYTES" f mode blob fsize
|
|||
|
|
while IFS= read -r -d '' f; do
|
|||
|
|
[ -n "$f" ] || continue
|
|||
|
|
# Reject control characters, absolute paths, and traversal outright.
|
|||
|
|
case "$f" in
|
|||
|
|
/*|*..*) note_bound "$side: rejected suspicious path name" ; continue ;;
|
|||
|
|
esac
|
|||
|
|
if printf '%s' "$f" | LC_ALL=C grep -q '[[:cntrl:]]'; then
|
|||
|
|
note_bound "$side: rejected path name containing control characters"
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
case "$f" in
|
|||
|
|
package-lock.json|*.lock|*.tsbuildinfo|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.zip)
|
|||
|
|
note_bound "$side/$f excluded (lockfile/binary)"; continue ;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
# Only regular blobs (100644/100755). Mode 120000 is a symlink and 160000 a
|
|||
|
|
# submodule; both are skipped rather than followed.
|
|||
|
|
mode="$(git -C "$dir" ls-tree HEAD -- "$f" | awk '{print $1}')"
|
|||
|
|
case "$mode" in
|
|||
|
|
100644|100755) ;;
|
|||
|
|
120000) note_bound "$side/$f excluded (symlink)"; continue ;;
|
|||
|
|
"") note_bound "$side/$f not present at head (deleted)"; continue ;;
|
|||
|
|
*) note_bound "$side/$f excluded (unsupported git mode $mode)"; continue ;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
blob="$(git -C "$dir" rev-parse "HEAD:$f" 2>/dev/null)" || {
|
|||
|
|
note_bound "$side/$f unreadable at head"; continue; }
|
|||
|
|
fsize="$(git -C "$dir" cat-file -s "$blob")"
|
|||
|
|
if [ "$fsize" -gt "$FILE_MAX_BYTES" ]; then
|
|||
|
|
note_bound "$side/$f excluded ($fsize bytes exceeds per-file cap $FILE_MAX_BYTES)"; continue
|
|||
|
|
fi
|
|||
|
|
if [ "$fsize" -gt "$budget" ]; then
|
|||
|
|
note_bound "$side/$f excluded (context budget exhausted)"; continue
|
|||
|
|
fi
|
|||
|
|
if ! git -C "$dir" cat-file -p "$blob" | LC_ALL=C grep -Iq .; then
|
|||
|
|
note_bound "$side/$f excluded (binary)"; continue
|
|||
|
|
fi
|
|||
|
|
budget=$((budget - fsize))
|
|||
|
|
{
|
|||
|
|
printf '===== FILE %s (%s at reviewed head) =====\n' "$f" "$side"
|
|||
|
|
git -C "$dir" cat-file -p "$blob"
|
|||
|
|
printf '\n'
|
|||
|
|
} >>"$files_section"
|
|||
|
|
done < <(gh api "repos/$repo/pulls/$pr/files" --paginate --jq '.[].filename' --raw-output \
|
|||
|
|
| jq -Rrj '. + " |