mirror of
https://github.com/Sea-Haven-Industries/shoc-pr-review-runner.git
synced 2026-09-30 09:03:11 +00:00
106 lines
4.2 KiB
Bash
106 lines
4.2 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Shared helpers for the SHOC PR review runner. Sourced by every script.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
: "${WORKSPACE_DIR:?WORKSPACE_DIR must be set}"
|
||
|
|
ARTIFACTS_DIR="${ARTIFACTS_DIR:-$WORKSPACE_DIR/artifacts}"
|
||
|
|
# The gate table records what actually executed, so it must NOT live inside the
|
||
|
|
# workspace: the build gates execute PR-authored code (npm/dotnet lifecycle
|
||
|
|
# scripts), and anything under the workspace is trivially writable by that code.
|
||
|
|
# STATE_DIR defaults outside the workspace; duplicate keys are rejected at read
|
||
|
|
# time so a tampered table fails the run instead of forging a PASS.
|
||
|
|
STATE_DIR="${STATE_DIR:-${RUNNER_TEMP:-$WORKSPACE_DIR}/runner-state}"
|
||
|
|
GATE_STATUS_FILE="${GATE_STATUS_FILE:-$STATE_DIR/gate-status.tsv}"
|
||
|
|
LOG_DIR="${LOG_DIR:-$ARTIFACTS_DIR/logs}"
|
||
|
|
|
||
|
|
mkdir -p "$ARTIFACTS_DIR" "$LOG_DIR" "$STATE_DIR"
|
||
|
|
chmod 700 "$STATE_DIR" 2>/dev/null || true
|
||
|
|
|
||
|
|
log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*" >&2; }
|
||
|
|
|
||
|
|
die() {
|
||
|
|
log "ERROR: $*"
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# record_gate <key> <PASS|FAIL|NOT_APPLICABLE|NOT_RUN|BLOCKED> [detail]
|
||
|
|
# Appends to the machine-readable gate table consumed by evidence generation and
|
||
|
|
# output validation. A gate is only ever PASS because the command that proves it
|
||
|
|
# actually ran and exited 0.
|
||
|
|
record_gate() {
|
||
|
|
local key="$1" status="$2" detail="${3:-}"
|
||
|
|
case "$status" in
|
||
|
|
PASS|FAIL|NOT_APPLICABLE|NOT_RUN|BLOCKED) ;;
|
||
|
|
*) die "invalid gate status '$status' for $key" ;;
|
||
|
|
esac
|
||
|
|
# Strip control characters from the detail so nothing can forge table rows.
|
||
|
|
detail="$(printf '%s' "$detail" | tr -d '\000-\037')"
|
||
|
|
printf '%s\t%s\t%s\n' "$key" "$status" "$detail" >>"$GATE_STATUS_FILE"
|
||
|
|
log "gate $key = $status${detail:+ ($detail)}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# run_gate <key> <logfile-basename> <cmd...>
|
||
|
|
# Runs the command, captures combined output to the log, records PASS/FAIL.
|
||
|
|
# Returns the command's exit code so callers can decide whether to block
|
||
|
|
# dependent gates.
|
||
|
|
#
|
||
|
|
# The command may be PR-authored code, so the Actions runner-command channels
|
||
|
|
# are removed from its environment: without them it cannot append to
|
||
|
|
# $GITHUB_ENV / $GITHUB_PATH to poison later steps, or write step outputs.
|
||
|
|
run_gate() {
|
||
|
|
local key="$1" logname="$2"
|
||
|
|
shift 2
|
||
|
|
local logfile="$LOG_DIR/$logname"
|
||
|
|
log "running gate $key: $*"
|
||
|
|
local rc=0
|
||
|
|
# RUNNER_TEMP is unset too: the Actions file commands live at
|
||
|
|
# $RUNNER_TEMP/_runner_file_commands/*, so leaving it set would let the build
|
||
|
|
# re-acquire the $GITHUB_ENV / $GITHUB_PATH channel by globbing that directory.
|
||
|
|
env -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_OUTPUT -u GITHUB_STATE \
|
||
|
|
-u GITHUB_STEP_SUMMARY -u ACTIONS_RUNTIME_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_TOKEN \
|
||
|
|
-u ACTIONS_ID_TOKEN_REQUEST_URL -u RUNNER_TEMP -u STATE_DIR -u GATE_STATUS_FILE \
|
||
|
|
"$@" >>"$logfile" 2>&1 || rc=$?
|
||
|
|
if [ "$rc" -eq 0 ]; then
|
||
|
|
record_gate "$key" PASS "log: logs/$logname"
|
||
|
|
else
|
||
|
|
record_gate "$key" FAIL "exit $rc, log: logs/$logname"
|
||
|
|
fi
|
||
|
|
return "$rc"
|
||
|
|
}
|
||
|
|
|
||
|
|
# assert_gate_table_intact
|
||
|
|
# Fails closed if any gate key appears more than once. The runner records each
|
||
|
|
# key exactly once, so a duplicate means someone else wrote to the table. This
|
||
|
|
# catches both override-by-append and pre-seeding (writing a forged PASS for a
|
||
|
|
# key before the runner records the genuine result): either way the genuine row
|
||
|
|
# lands alongside the forged one and the duplicate is detected.
|
||
|
|
# MUST be called before any gate_status() read that informs a decision.
|
||
|
|
assert_gate_table_intact() {
|
||
|
|
[ -f "$GATE_STATUS_FILE" ] || return 0
|
||
|
|
local dupes
|
||
|
|
dupes="$(cut -f1 "$GATE_STATUS_FILE" | sort | uniq -d)"
|
||
|
|
if [ -n "$dupes" ]; then
|
||
|
|
log "duplicate gate keys detected (table tampering or a script bug):"
|
||
|
|
printf '%s\n' "$dupes" >&2
|
||
|
|
die "gate table integrity check failed"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# gate_status <key> -> prints the recorded status, or NOT_RUN if absent.
|
||
|
|
# First-wins: the runner records each key once, so an appended row can never
|
||
|
|
# override a genuine result even if the integrity check is bypassed.
|
||
|
|
gate_status() {
|
||
|
|
local key="$1"
|
||
|
|
awk -F'\t' -v k="$key" '$1==k && !seen {s=$2; seen=1} END{print (seen?s:"NOT_RUN")}' \
|
||
|
|
"$GATE_STATUS_FILE" 2>/dev/null || echo "NOT_RUN"
|
||
|
|
}
|
||
|
|
|
||
|
|
# require_env <name>...
|
||
|
|
require_env() {
|
||
|
|
local n
|
||
|
|
for n in "$@"; do
|
||
|
|
[ -n "${!n:-}" ] || die "required environment variable $n is not set"
|
||
|
|
done
|
||
|
|
}
|