run.sh 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env bash
  2. # Offline self-test for github-ops scripts. No network required — exercises the
  3. # contract + the gate-safety skip paths (graceful exit 7), not live GitHub data.
  4. set -uo pipefail
  5. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  6. ROOT="$(cd "$HERE/.." && pwd)"
  7. SCRIPTS="$ROOT/scripts"
  8. CI="$SCRIPTS/check-issues.sh"
  9. SP="$SCRIPTS/check-security-posture.sh"
  10. pass=0; fail=0
  11. ok() { echo " PASS $1"; pass=$((pass+1)); }
  12. no() { echo " FAIL $1"; fail=$((fail+1)); }
  13. expect() { if [ "$2" = "$3" ]; then ok "$1 (exit $3)"; else no "$1 (want $2 got $3)"; fi; }
  14. echo "-- check-issues.sh (offline contract + skip paths) --"
  15. bash -n "$CI" && ok "bash -n clean" || no "bash -n"
  16. bash "$CI" --help >/dev/null 2>&1; expect "--help" 0 $?
  17. bash "$CI" --frobnicate >/dev/null 2>&1; expect "unknown flag -> usage" 2 $?
  18. # Non-github remote must skip with exit 7 and NEVER hit the network.
  19. T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
  20. git -C "$T" init -q
  21. git -C "$T" remote add origin "/some/local/path.git"
  22. ( cd "$T" && bash "$CI" --remote origin >/dev/null 2>&1 ); expect "non-github remote -> unavailable" 7 $?
  23. # Advisory mode on a non-github remote must be SILENT (no stderr) and exit 7 —
  24. # this is the gate-safety contract: an unusable check never disturbs a push.
  25. out="$( cd "$T" && bash "$CI" --advisory --remote origin 2>&1 )"; rc=$?
  26. if [ "$rc" -eq 7 ] && [ -z "$out" ]; then ok "advisory non-github -> silent exit 7"
  27. else no "advisory non-github (rc=$rc, stderr='$out')"; fi
  28. # Missing remote -> skip 7 (git remote get-url fails; no network).
  29. ( cd "$T" && bash "$CI" --remote nope-xyz >/dev/null 2>&1 ); expect "missing remote -> unavailable" 7 $?
  30. echo
  31. echo "-- check-security-posture.sh (offline contract + skip paths) --"
  32. bash -n "$SP" && ok "sp: bash -n clean" || no "sp: bash -n"
  33. bash "$SP" --help >/dev/null 2>&1; expect "sp: --help" 0 $?
  34. # --help must advertise EXAMPLES so the tool is discoverable.
  35. if bash "$SP" --help 2>&1 | grep -q "Examples:"; then ok "sp: --help has EXAMPLES"
  36. else no "sp: --help missing EXAMPLES"; fi
  37. bash "$SP" --frobnicate >/dev/null 2>&1; expect "sp: unknown flag -> usage" 2 $?
  38. # Malformed OWNER/REPO is a usage error, never a network call.
  39. bash "$SP" --repo "not-a-valid-spec" >/dev/null 2>&1; expect "sp: bad --repo shape -> usage" 2 $?
  40. # --repo and --org are mutually exclusive.
  41. bash "$SP" --repo a/b --org c >/dev/null 2>&1; expect "sp: --repo + --org -> usage" 2 $?
  42. # Non-github remote must skip with exit 7 and NEVER hit the network.
  43. ( cd "$T" && bash "$SP" --remote origin >/dev/null 2>&1 ); expect "sp: non-github remote -> unavailable" 7 $?
  44. # Advisory mode on a non-github remote must be SILENT and exit 7.
  45. out="$( cd "$T" && bash "$SP" --advisory --remote origin 2>&1 )"; rc=$?
  46. if [ "$rc" -eq 7 ] && [ -z "$out" ]; then ok "sp: advisory non-github -> silent exit 7"
  47. else no "sp: advisory non-github (rc=$rc, stderr='$out')"; fi
  48. # Missing remote -> skip 7.
  49. ( cd "$T" && bash "$SP" --remote nope-xyz >/dev/null 2>&1 ); expect "sp: missing remote -> unavailable" 7 $?
  50. # --commands emits the review banner on stderr (offline path: banner prints before
  51. # any network work would, on a non-github remote it still skips — so assert the
  52. # banner via the bundled help text instead, which is fully offline).
  53. # The review banner string must be present in the source contract.
  54. if grep -q "review before running — these change repo settings" "$SP"; then ok "sp: review banner string present"
  55. else no "sp: review banner missing"; fi
  56. # The SECURITY.md template asset must exist and be non-trivial.
  57. if [ -s "$ROOT/assets/SECURITY.md.template" ] && grep -q "Reporting a Vulnerability" "$ROOT/assets/SECURITY.md.template"; then
  58. ok "sp: SECURITY.md.template asset present"
  59. else no "sp: SECURITY.md.template asset missing/empty"; fi
  60. # Read-only guarantee. The ONLY executor in this script is `runner gh api …`
  61. # (every -X PUT/PATCH lives inside an emitted *_cmd string, never executed). Assert
  62. # no `runner gh api` invocation carries a mutating verb.
  63. if grep -E 'runner gh api' "$SP" | grep -Eq '\-X (PUT|PATCH|POST|DELETE)'; then
  64. no "sp: found an executed mutating gh api call (must be read-only)"
  65. else ok "sp: no executed mutating gh api call (read-only)"; fi
  66. # And every mutating verb that DOES appear must be inside a quoted command string
  67. # (assigned to a *_cmd var), proving it is emitted-as-text only.
  68. if grep -nE '\-X (PUT|PATCH|POST|DELETE)' "$SP" | grep -vqE '_cmd='; then
  69. no "sp: a mutating verb appears outside an emitted *_cmd string"
  70. else ok "sp: all mutating verbs are emitted text only"; fi
  71. echo
  72. echo "-- repo-scorecard.sh (offline contract + orchestration + read-only proof) --"
  73. RS="$SCRIPTS/repo-scorecard.sh"
  74. bash -n "$RS" && ok "rs: bash -n clean" || no "rs: bash -n"
  75. bash "$RS" --help >/dev/null 2>&1; expect "rs: --help" 0 $?
  76. if bash "$RS" --help 2>&1 | grep -q "Examples:"; then ok "rs: --help has EXAMPLES"
  77. else no "rs: --help missing EXAMPLES"; fi
  78. # The scoring rubric must be documented in the header (transparent, auditable).
  79. if bash "$RS" --help 2>&1 | grep -q "SCORING MODEL"; then ok "rs: --help documents SCORING MODEL"
  80. else no "rs: --help missing SCORING MODEL"; fi
  81. bash "$RS" --frobnicate >/dev/null 2>&1; expect "rs: unknown flag -> usage" 2 $?
  82. # Malformed OWNER/REPO is a usage error, never a network call.
  83. bash "$RS" --repo "not-a-valid-spec" >/dev/null 2>&1; expect "rs: bad --repo shape -> usage" 2 $?
  84. # --repo and --org are mutually exclusive.
  85. bash "$RS" --repo a/b --org c >/dev/null 2>&1; expect "rs: --repo + --org -> usage" 2 $?
  86. # --min-score must be an integer.
  87. bash "$RS" --min-score xx >/dev/null 2>&1; expect "rs: bad --min-score -> usage" 2 $?
  88. # Non-github remote must skip with exit 7 and NEVER hit the network.
  89. ( cd "$T" && bash "$RS" --remote origin >/dev/null 2>&1 ); expect "rs: non-github remote -> unavailable" 7 $?
  90. # Missing remote -> skip 7.
  91. ( cd "$T" && bash "$RS" --remote nope-xyz >/dev/null 2>&1 ); expect "rs: missing remote -> unavailable" 7 $?
  92. # Orchestration: it MUST call the sibling auditors by name (the reuse is the point).
  93. if grep -q "check-security-posture.sh" "$RS"; then ok "rs: references check-security-posture.sh"
  94. else no "rs: does not reference check-security-posture.sh"; fi
  95. if grep -q "check-issues.sh" "$RS"; then ok "rs: references check-issues.sh"
  96. else no "rs: does not reference check-issues.sh"; fi
  97. # Read-only guarantee: no executed mutating gh verb anywhere. Every gh call must
  98. # be a GET (the remediation pointers it prints are text, not executed). Assert no
  99. # `gh api -X PUT/PATCH/POST/DELETE` and no `gh repo edit`/`gh release create` etc.
  100. if grep -E '\bgh (api )?-X (PUT|PATCH|POST|DELETE)' "$RS" | grep -vqE '^\s*#'; then
  101. no "rs: found an executed mutating gh -X call (must be read-only)"
  102. else ok "rs: no executed mutating gh -X call (read-only)"; fi
  103. # Belt-and-braces: every `runner gh …` (the only network executor) is a read-only
  104. # subcommand — `gh api <GET path>` or `gh repo list`. No mutating subcommand runs.
  105. if grep -nE 'runner gh ' "$RS" | grep -Evq 'runner gh (api|repo list)'; then
  106. no "rs: a 'runner gh' call uses a non-read-only subcommand"
  107. else ok "rs: every executed 'runner gh' is read-only (api / repo list)"; fi
  108. # And mutating gh subcommands, where they appear, are inside printed fix strings only
  109. # (the remediation pointers), never executed. Verify they sit on addfix/echo lines.
  110. if grep -nE 'gh (release create|repo edit|release delete|secret set|pr merge)' "$RS" \
  111. | grep -vqE 'addfix|→'; then
  112. no "rs: a mutating gh subcommand appears outside a printed remediation string"
  113. else ok "rs: mutating gh subcommands only appear as printed remediation text"; fi
  114. echo
  115. echo "-- terminal design system (term.sh adoption + ASCII fallback) --"
  116. # All three auditors must source the shared toolkit, not hand-roll ANSI.
  117. for s in "$CI" "$SP" "$RS"; do
  118. b="$(basename "$s")"
  119. if grep -q '_lib/term.sh' "$s"; then ok "$b sources _lib/term.sh"
  120. else no "$b does not source _lib/term.sh"; fi
  121. done
  122. LIBTERM="$ROOT/../_lib/term.sh"
  123. if [ -f "$LIBTERM" ]; then
  124. ok "term.sh present"
  125. # Under TERM_ASCII=1 every framing primitive must fall back to pure ASCII
  126. # (design principle #3: every glyph has a registered ASCII proxy).
  127. marks="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; printf "%s%s%s%s%s%s%s%s%s%s%s" \
  128. "$(term_mark ok)" "$(term_mark bad)" "$(term_mark warn)" "$(term_mark na)" \
  129. "$(term_mark unknown)" "$(term_header hdr)" "$TERM_ARROW" \
  130. "$(term_panel_open github-ops PANEL meta)" "$(term_panel_line body)" \
  131. "$(term_section "" sect 3)" "$(term_panel_close hk "$(term_health warning x)")"')"
  132. if printf '%s' "$marks" | LC_ALL=C grep -q '[^[:print:][:cntrl:]]'; then
  133. no "term.sh TERM_ASCII=1 still emits non-ASCII bytes"
  134. else ok "term.sh TERM_ASCII=1 primitives are pure ASCII"; fi
  135. # A fallback that silently drops the glyph (empty) is a bug, not a fallback.
  136. m="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; term_mark ok')"
  137. [ -n "$m" ] && ok "term_mark renders non-empty in ASCII mode" || no "term_mark ok is empty"
  138. else
  139. no "term.sh missing at $LIBTERM"
  140. fi
  141. echo
  142. echo "=== $pass passed, $fail failed ==="
  143. [ "$fail" -eq 0 ]