Files
Alexey MilovidovandClaude Opus 4.8 8e7288ef39 good-prs: address review feedback on report.sh reliability
Addresses the AI Review findings on PR #108767
(https://github.com/ClickHouse/ClickHouse/pull/108767):

1. Classify checks by `gh pr checks`' own `bucket` field (`pass` / `fail` /
   `pending` / `skipping` / `cancel`) instead of a narrow raw-`state`
   allow-list. The old code only recognized `SUCCESS` / `FAILURE` / `PENDING`
   / `IN_PROGRESS` / `ABSENT` for the `CH Inc sync` check, so a sync in a state
   like `QUEUED`, `CANCELLED`, `TIMED_OUT`, `STARTUP_FAILURE`, or `SKIPPED`
   matched nothing and the PR was silently dropped from the report.
   `classify.sh` now emits a normalized `GREEN` / `FAILED` / `INPROG` /
   `NOSYNC` label and the downstream `awk`/`case` were updated to match.

2. Fail loudly on a real `gh` error instead of silently dropping the row.
   With `--json`, `gh pr checks` exits 0 and prints a JSON array whenever it
   could read the checks (even failing/pending ones), so a non-zero exit means
   either "no checks reported" (legitimate, kept as `NO_CHECKS` and excluded)
   or a genuine API / auth / rate-limit failure. The helpers now distinguish
   the two: on a genuine failure they print the original `gh` diagnostic and
   `exit 255`, which stops `xargs` and makes `set -e` abort the run. The
   `2>/dev/null` on the `xargs` lines was removed so the diagnostic is visible.
   `approval.sh` got the same treatment.

3. Pin `--repo ClickHouse/ClickHouse` on every PR-scoped `gh` call. The report
   already hardcodes `ClickHouse/ClickHouse` URLs, so the data source must match
   them; pinning makes the skill correct regardless of the directory it is run
   from.

Adds `test.sh`, which runs `report.sh` against a stubbed `gh` (no network) and
covers the `bucket`->label mapping (including `QUEUED`->`INPROG`,
`CANCELLED`->`FAILED`, `SKIPPED`->`NOSYNC`), exclusion of PRs with a non-sync
failing check, empty input, `--repo` pinning, the `no checks reported` case,
and the fail-loud-on-real-error behaviour.

This is developer tooling under `.claude/skills/` only; it changes no product
code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-28 23:57:35 +00:00

196 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Focused tests for report.sh. No network access: `gh` is replaced by a stub on
# PATH that serves canned fixtures, so the tests exercise report.sh's own logic
# (bucket -> label classification, the "only CH Inc sync is non-green" criterion,
# empty-input handling, --repo pinning, and fail-loud-on-real-error behaviour).
#
# Usage: bash test.sh
#
set -uo pipefail
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPORT="$HERE/report.sh"
RC=0
ok() { printf ' ok - %s\n' "$*"; }
bad() { printf ' FAIL - %s\n' "$*"; RC=1; }
# Write the `gh` stub into <dir>/bin. It dispatches on the gh subcommand and reads
# canned fixtures from $GH_STUB_DIR, logging every invocation to $GH_STUB_LOG.
write_stub()
{
local bin="$1/bin"
mkdir -p "$bin"
cat > "$bin/gh" <<'STUB'
#!/usr/bin/env bash
set -uo pipefail
printf '%s\n' "$*" >> "$GH_STUB_LOG"
sub="${1:-} ${2:-}"
case "$sub" in
"api user")
cat "$GH_STUB_DIR/me" ;;
"pr list")
mode=""; who=""
while [ "$#" -gt 0 ]; do
case "$1" in
--author) mode=author; who="$2"; shift 2 ;;
--assignee) mode=assignee; who="$2"; shift 2 ;;
*) shift ;;
esac
done
f="$GH_STUB_DIR/list/${mode}_${who}.tsv"
[ -f "$f" ] && cat "$f"
;;
"pr checks")
n="$3"
if [ -f "$GH_STUB_DIR/checks/$n.err" ]; then cat "$GH_STUB_DIR/checks/$n.err" >&2; exit 1; fi
if [ -f "$GH_STUB_DIR/checks/$n.nochecks" ]; then echo "no checks reported on the 'br-$n' branch" >&2; exit 1; fi
cat "$GH_STUB_DIR/checks/$n.json"
;;
"pr view")
n="$3"
if [ -f "$GH_STUB_DIR/view/$n.err" ]; then cat "$GH_STUB_DIR/view/$n.err" >&2; exit 1; fi
cat "$GH_STUB_DIR/view/$n.tsv"
;;
*)
echo "gh stub: unhandled args: $*" >&2; exit 99 ;;
esac
exit 0 # success branches return 0 even when a list fixture is absent (empty list)
STUB
chmod +x "$bin/gh"
}
# run_report <stub-dir> -> sets globals: OUT, ERRF, EXIT
run_report()
{
local d="$1"
OUT="$d/out"; ERRF="$d/err"
PATH="$d/bin:$PATH" GH_STUB_DIR="$d/fix" GH_STUB_LOG="$d/calls.log" \
bash "$REPORT" > "$OUT" 2> "$ERRF"
EXIT=$?
}
row_has() { # <out> <pr-num> <label> <desc>
if grep -F "#$2" "$1" | grep -qF "$3"; then ok "$4"; else bad "$4"; fi
}
not_present() { # <out> <pr-num> <desc>
if grep -qF "#$1" "$2"; then bad "$3"; else ok "$3"; fi
}
# Every PR-scoped gh call must be pinned to ClickHouse/ClickHouse.
assert_repo_pinned() { # <calls.log> <desc>
if grep -E '^pr (list|checks|view)' "$1" | grep -vq -- '--repo ClickHouse/ClickHouse'; then
bad "$2"
else
ok "$2"
fi
}
############################ Test A: classification ############################
echo "Test A: bucket -> label classification and the only-sync criterion"
A=$(mktemp -d); write_stub "$A"; mkdir -p "$A/fix/list" "$A/fix/checks" "$A/fix/view"
printf 'testuser\n' > "$A/fix/me"
{
printf '101\ttestuser\tFully green PR\n'
printf '102\ttestuser\tSync failed PR\n'
printf '103\ttestuser\tSync in progress PR\n'
printf '104\ttestuser\tBackport, no sync check\n'
printf '105\ttestuser\tNon-sync check failing\n'
printf '106\ttestuser\tMerged between list and view\n'
printf '107\ttestuser\tSync QUEUED\n'
printf '108\ttestuser\tSync CANCELLED\n'
printf '109\ttestuser\tSync SKIPPED\n'
} > "$A/fix/list/author_testuser.tsv"
printf '201\tgroeneai\tGroeneai green PR\n' > "$A/fix/list/author_groeneai.tsv"
c() { printf '%s\n' "$2" > "$A/fix/checks/$1.json"; }
c 101 '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"},{"name":"Build","state":"SUCCESS","bucket":"pass"},{"name":"Tests","state":"SKIPPED","bucket":"skipping"},{"name":"PR","state":"SUCCESS","bucket":"pass"},{"name":"Mergeable Check","state":"SUCCESS","bucket":"pass"}]'
c 102 '[{"name":"CH Inc sync","state":"FAILURE","bucket":"fail"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 103 '[{"name":"CH Inc sync","state":"IN_PROGRESS","bucket":"pending"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 104 '[{"name":"Build","state":"SUCCESS","bucket":"pass"},{"name":"Docs","state":"SKIPPED","bucket":"skipping"}]'
c 105 '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"},{"name":"Tests","state":"FAILURE","bucket":"fail"}]'
c 106 '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 107 '[{"name":"CH Inc sync","state":"QUEUED","bucket":"pending"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 108 '[{"name":"CH Inc sync","state":"CANCELLED","bucket":"cancel"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 109 '[{"name":"CH Inc sync","state":"SKIPPED","bucket":"skipping"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
c 201 '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"},{"name":"Build","state":"SUCCESS","bucket":"pass"}]'
v() { printf '%s\t%s\n' "$2" "$3" > "$A/fix/view/$1.tsv"; }
v 101 OPEN true
v 102 OPEN false
v 103 OPEN false
v 104 OPEN false
v 106 MERGED false
v 107 OPEN false
v 108 OPEN false
v 109 OPEN false
v 201 OPEN true
run_report "$A"
[ "$EXIT" = 0 ] && ok "exit code 0" || { bad "exit code 0 (got $EXIT)"; cat "$ERRF"; }
row_has "$OUT" 101 GREEN "#101 fully green -> GREEN"
row_has "$OUT" 102 FAILED "#102 sync FAILURE -> FAILED"
row_has "$OUT" 103 INPROG "#103 sync IN_PROGRESS -> INPROG"
row_has "$OUT" 104 NOSYNC "#104 no sync check -> NOSYNC"
not_present 105 "$OUT" "#105 non-sync failure -> excluded"
not_present 106 "$OUT" "#106 merged -> excluded"
row_has "$OUT" 107 INPROG "#107 sync QUEUED (bucket pending) -> INPROG"
row_has "$OUT" 108 FAILED "#108 sync CANCELLED (bucket cancel) -> FAILED"
row_has "$OUT" 109 NOSYNC "#109 sync SKIPPED (bucket skipping) -> NOSYNC"
row_has "$OUT" 201 GREEN "#201 groeneai section rendered"
assert_repo_pinned "$A/calls.log" "every PR-scoped gh call pins --repo ClickHouse/ClickHouse"
rm -rf "$A"
############################ Test B: empty input ###############################
echo "Test B: no PRs at all -> clean empty sections, exit 0"
B=$(mktemp -d); write_stub "$B"; mkdir -p "$B/fix/list" "$B/fix/checks" "$B/fix/view"
printf 'testuser\n' > "$B/fix/me"
run_report "$B"
[ "$EXIT" = 0 ] && ok "exit code 0 on empty input" || { bad "exit code 0 on empty input (got $EXIT)"; cat "$ERRF"; }
if grep -q '## 1. Your own PRs' "$OUT"; then ok "section headers still rendered"; else bad "section headers still rendered"; fi
if grep -q '\[#' "$OUT"; then bad "no PR rows on empty input"; else ok "no PR rows on empty input"; fi
rm -rf "$B"
####################### Test C: real gh failure on checks ######################
echo "Test C: transient gh pr checks failure -> abort with diagnostic"
C=$(mktemp -d); write_stub "$C"; mkdir -p "$C/fix/list" "$C/fix/checks" "$C/fix/view"
printf 'testuser\n' > "$C/fix/me"
printf '301\ttestuser\tWill error\n' > "$C/fix/list/author_testuser.tsv"
printf 'HTTP 403: API rate limit exceeded\n' > "$C/fix/checks/301.err"
run_report "$C"
[ "$EXIT" != 0 ] && ok "non-zero exit on gh checks failure" || bad "non-zero exit on gh checks failure (got $EXIT)"
if grep -qF "'gh pr checks 301' failed" "$ERRF"; then ok "diagnostic names the failing PR"; else bad "diagnostic names the failing PR"; fi
if grep -qF "rate limit" "$ERRF"; then ok "original gh diagnostic preserved"; else bad "original gh diagnostic preserved"; fi
rm -rf "$C"
####################### Test D: real gh failure on view ########################
echo "Test D: transient gh pr view failure -> abort with diagnostic"
D=$(mktemp -d); write_stub "$D"; mkdir -p "$D/fix/list" "$D/fix/checks" "$D/fix/view"
printf 'testuser\n' > "$D/fix/me"
printf '401\ttestuser\tView errors\n' > "$D/fix/list/author_testuser.tsv"
printf '%s\n' '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"}]' > "$D/fix/checks/401.json"
printf 'HTTP 500: server error\n' > "$D/fix/view/401.err"
run_report "$D"
[ "$EXIT" != 0 ] && ok "non-zero exit on gh view failure" || bad "non-zero exit on gh view failure (got $EXIT)"
if grep -qF "'gh pr view 401' failed" "$ERRF"; then ok "diagnostic names the failing PR"; else bad "diagnostic names the failing PR"; fi
rm -rf "$D"
################## Test E: 'no checks reported' is not fatal ####################
echo "Test E: a 'no checks reported' PR is excluded, not fatal"
E=$(mktemp -d); write_stub "$E"; mkdir -p "$E/fix/list" "$E/fix/checks" "$E/fix/view"
printf 'testuser\n' > "$E/fix/me"
{ printf '501\ttestuser\tNo checks reported\n'; printf '502\ttestuser\tGreen PR\n'; } > "$E/fix/list/author_testuser.tsv"
: > "$E/fix/checks/501.nochecks"
printf '%s\n' '[{"name":"CH Inc sync","state":"SUCCESS","bucket":"pass"}]' > "$E/fix/checks/502.json"
v502() { printf 'OPEN\ttrue\n' > "$E/fix/view/502.tsv"; }; v502
run_report "$E"
[ "$EXIT" = 0 ] && ok "exit code 0 (no-checks is legitimate)" || { bad "exit code 0 (got $EXIT)"; cat "$ERRF"; }
not_present 501 "$OUT" "#501 with no checks -> excluded"
row_has "$OUT" 502 GREEN "#502 green PR still rendered alongside excluded one"
rm -rf "$E"
echo
if [ "$RC" = 0 ]; then echo "ALL TESTS PASSED"; else echo "SOME TESTS FAILED"; fi
exit "$RC"