diff --git a/.github/scripts/observe-go-cache.sh b/.github/scripts/observe-go-cache.sh index ce3a5005..bf6ec7a0 100644 --- a/.github/scripts/observe-go-cache.sh +++ b/.github/scripts/observe-go-cache.sh @@ -4,6 +4,26 @@ set -euo pipefail export LC_ALL=C prefix='[Test] Validate Go Project - Isolated PostgreSQL / ' fail() { printf 'native-go-cache: %s\n' "$*" >&2; exit 2; } +diagnose_jobs() { + local summary + summary="$(jq -cse --argjson run "$run" --argjson attempt "$attempt" --arg head "$head" --arg prefix "$prefix" ' + def positive: type == "number" and . > 0 and floor == .; + (if length == 1 then .[0] else error("expected one job inventory") end) | + if type == "array" and length > 0 and + all(.[]; type == "object" and (.total_count | positive) and (.jobs | type == "array")) + then [.[].jobs[]] as $all | + [$all[] | select(.name | type == "string") | select(.name | startswith($prefix))] as $fixture | + {pages:length, reported_totals:([.[].total_count] | unique | .[0:8]), + total_count_variants:([.[].total_count] | unique | length), + fetched_jobs:($all | length), duplicate_ids:(($all | length) - ($all | map(.id) | unique | length)), + fixture_jobs:($fixture | length), fixture_invalid:([$fixture[] | select( + ((.id | positive) and .run_id == $run and .run_attempt == $attempt and .head_sha == $head and + .status == "completed" and .conclusion == "success" and (.steps | type == "array") and + ([.steps[].name] | length == (unique | length))) | not)] | length)} + else {state:"invalid_shape"} end + ' "$jobs" 2>/dev/null)" || summary='{"state":"unreadable"}' + printf 'native-go-cache: job-evidence-summary=%s\n' "$summary" >&2 +} validate_jobs() { jq -e --argjson run "$run" --argjson attempt "$attempt" --arg head "$head" --arg prefix "$prefix" ' def positive: type == "number" and . > 0 and floor == .; @@ -19,7 +39,10 @@ validate_jobs() { all($fixture[]; (.id | positive) and .run_id == $run and .run_attempt == $attempt and .head_sha == $head and .status == "completed" and .conclusion == "success" and (.steps | type == "array") and ([.steps[].name] | length == (unique | length)))) - ' "$jobs" >/dev/null 2>&1 || fail 'incomplete, stale or unsuccessful current-attempt jobs' + ' "$jobs" >/dev/null 2>&1 || { + diagnose_jobs + fail 'incomplete, stale or unsuccessful current-attempt jobs' + } } normalize_log() { sed -E 's/^[0-9]{4}-[0-9T:.+-]+Z //; s/^Cache saved with the key: /Cache saved with key: /' "$1" diff --git a/.github/tests/test-go-cache-native-observer.sh b/.github/tests/test-go-cache-native-observer.sh index f2fc5233..eb28594d 100644 --- a/.github/tests/test-go-cache-native-observer.sh +++ b/.github/tests/test-go-cache-native-observer.sh @@ -47,6 +47,47 @@ reject_jobs '.[1].jobs[0].head_sha = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"' reject_jobs '.[1].jobs[0].status = "in_progress"' reject_jobs '.[1].jobs[0].name += " unexpected"' reject_jobs '.[1].jobs[1].steps |= map(select(.name != "Save Go module cache"))' +reject_jobs_with_summary() { + reject_jobs "$1" + local summary + summary="$(sed -n 's/^native-go-cache: job-evidence-summary=//p' "$scratch/error")" + [[ -n "$summary" ]] || fail 'rejected jobs have no sanitized diagnostic' + jq -e ' + if keys == ["state"] then .state | IN("invalid_shape", "unreadable") + else keys == ["duplicate_ids", "fetched_jobs", "fixture_invalid", "fixture_jobs", "pages", "reported_totals", "total_count_variants"] and + (.reported_totals | length <= 8 and all(.[]; type == "number" and . > 0 and floor == .)) and + (del(.reported_totals) | all(.[]; type == "number" and . >= 0 and floor == .)) end + ' <<<"$summary" >/dev/null || fail 'diagnostic exposed unapproved fields or unbounded values' + jq -e "$2" <<<"$summary" >/dev/null || fail 'diagnostic does not identify the rejected invariant' +} +reject_jobs_with_summary '.[0].total_count = 10' \ + '.pages == 2 and .reported_totals == [9,10] and .fetched_jobs == 9 and .duplicate_ids == 0 and .fixture_jobs == 9' +reject_jobs_with_summary '.[1].jobs |= .[1:]' \ + '.reported_totals == [9] and .fetched_jobs == 8 and .duplicate_ids == 0 and .fixture_jobs == 8' +reject_jobs_with_summary '.[1].jobs[0] = .[1].jobs[1]' \ + '.fetched_jobs == 9 and .duplicate_ids == 1 and .fixture_jobs == 9' +reject_jobs_with_summary '.[1].jobs[0].run_attempt = 1' \ + '.fixture_invalid == 1 and .fixture_jobs == 9 and .duplicate_ids == 0' +reject_jobs_with_summary '.[1].jobs[0].status = "in_progress"' \ + '.fixture_invalid == 1 and .fixture_jobs == 9' +reject_jobs_with_summary '. + [range(10;22) | {total_count:.,jobs:[]}]' \ + '.pages == 14 and .total_count_variants == 13 and (.reported_totals | length == 8)' +# Only scalar counts may leave rejected evidence; never echo native names, +# identifiers, arbitrary response text or parser diagnostics. +reject_jobs_with_summary '.[0].total_count = "private-response-sentinel"' \ + '. == {state:"invalid_shape"}' +if grep -q 'private-response-sentinel' "$scratch/error"; then fail 'diagnostic disclosed raw evidence'; fi +printf 'not-json private-response-sentinel\n' >"$scratch/jobs.json" +if check pr >"$scratch/out" 2>"$scratch/error"; then fail 'malformed evidence passed'; fi +[[ ! -s "$scratch/out" ]] || fail 'malformed evidence published success' +grep -qx 'native-go-cache: job-evidence-summary={"state":"unreadable"}' "$scratch/error" || fail 'malformed evidence lacks a safe diagnostic' +if grep -q 'private-response-sentinel' "$scratch/error"; then fail 'parser disclosed raw evidence'; fi +for evidence in '' ' ' '{} {}'; do + printf '%s\n' "$evidence" >"$scratch/jobs.json" + if check pr >"$scratch/out" 2>"$scratch/error"; then fail 'empty or multiple-root evidence passed'; fi + [[ ! -s "$scratch/out" ]] || fail 'invalid JSON stream published success' + grep -qx 'native-go-cache: job-evidence-summary={"state":"unreadable"}' "$scratch/error" || fail 'invalid JSON stream lacks one safe diagnostic' +done cp "$scratch/good-jobs.json" "$scratch/jobs.json" cp "$scratch/logs/108.log" "$scratch/good-scan.log" printf 'Cache saved with the key: setup-go-unsafe\n' >>"$scratch/logs/108.log"