| 1 | #!/usr/bin/env sh |
| 2 | # π€ ops validate β run every check this repo can run locally, in one command. |
| 3 | # |
| 4 | # ./deploy/ops validate # everything (what to run before opening a PR) |
| 5 | # ./deploy/ops validate go # Go build + vet + tests |
| 6 | # ./deploy/ops validate web # node --check on the ES modules + npm test |
| 7 | # ./deploy/ops validate shell # sh -n (+ shellcheck if present) on deploy/ |
| 8 | # ./deploy/ops validate infra # tofu fmt + validate on the dev-mesh stack |
| 9 | # |
| 10 | # Why this is a verb: the checks are spread across four toolchains, and every |
| 11 | # one of them is easy to forget β the shell scripts and the tofu stack in |
| 12 | # particular have no test suite and are only ever caught by running these by |
| 13 | # hand. A contributor (human or agent) should not have to reconstruct the list |
| 14 | # from CLAUDE.md; ops is where operational knowledge lives, and "what do I run |
| 15 | # before pushing" is exactly that. |
| 16 | # |
| 17 | # Non-goals: this never touches a box, a credential, or the network beyond what |
| 18 | # `go test` and `npm test` already do (both are hermetic β httptest servers and |
| 19 | # Node's test runner). It is safe to run at any time, on any branch. |
| 20 | # |
| 21 | # Exit status is the number of FAILED sections (0 = all good), and every section |
| 22 | # runs even when an earlier one fails β one pass tells you everything that is |
| 23 | # broken, rather than making you re-run after each fix. |
| 24 | set -eu |
| 25 | |
| 26 | here="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" |
| 27 | root="$(CDPATH= cd -- "$here/.." && pwd)" |
| 28 | cd "$root" |
| 29 | |
| 30 | bold=''; red=''; green=''; yellow=''; off='' |
| 31 | if [ -t 1 ]; then |
| 32 | bold=$(printf '\033[1m'); red=$(printf '\033[31m'); green=$(printf '\033[32m') |
| 33 | yellow=$(printf '\033[33m'); off=$(printf '\033[0m') |
| 34 | fi |
| 35 | |
| 36 | failed=0 |
| 37 | skipped=0 |
| 38 | |
| 39 | section() { printf '\n%s%s%s\n' "$bold" "$1" "$off"; } |
| 40 | ok() { printf ' %sok%s %s\n' "$green" "$off" "$1"; } |
| 41 | skip() { printf ' %sskip%s %s\n' "$yellow" "$off" "$1"; skipped=$((skipped + 1)); } |
| 42 | bad() { printf ' %sFAIL%s %s\n' "$red" "$off" "$1"; failed=$((failed + 1)); } |
| 43 | |
| 44 | # run <label> <command...>: run it, show output only when it fails. Keeping a |
| 45 | # passing run silent is what makes the summary readable; a failure prints |
| 46 | # everything, because that is the moment you need it. |
| 47 | run() { |
| 48 | label="$1"; shift |
| 49 | out="$(mktemp -t lchat-validate.XXXXXX)" |
| 50 | if "$@" >"$out" 2>&1; then |
| 51 | ok "$label" |
| 52 | else |
| 53 | bad "$label" |
| 54 | sed 's/^/ /' "$out" |
| 55 | fi |
| 56 | rm -f "$out" |
| 57 | } |
| 58 | |
| 59 | validate_go() { |
| 60 | section "Go" |
| 61 | if ! command -v go >/dev/null 2>&1; then skip "go not installed"; return; fi |
| 62 | run "build (all packages)" go build ./... |
| 63 | # vet is advisory here: the tree carries a pre-existing finding in |
| 64 | # checkpoint_test.go, so a failure must not mask a real regression elsewhere. |
| 65 | # Shown, not counted. |
| 66 | if go vet ./... >/dev/null 2>&1; then |
| 67 | ok "vet" |
| 68 | else |
| 69 | printf ' %swarn%s vet reported findings:\n' "$yellow" "$off" |
| 70 | go vet ./... 2>&1 | sed 's/^/ /' || true |
| 71 | fi |
| 72 | run "tests" go test ./... |
| 73 | } |
| 74 | |
| 75 | validate_web() { |
| 76 | section "Web" |
| 77 | if command -v node >/dev/null 2>&1; then |
| 78 | # The ES modules are never executed by the test suite, so a syntax error |
| 79 | # ships silently β `node --check` is the only thing standing in the way. |
| 80 | bad_files='' |
| 81 | for f in web/static/*.js web/static/apps/*.js; do |
| 82 | [ -f "$f" ] || continue |
| 83 | node --check "$f" >/dev/null 2>&1 || bad_files="$bad_files $f" |
| 84 | done |
| 85 | if [ -n "$bad_files" ]; then |
| 86 | bad "node --check β$bad_files" |
| 87 | for f in $bad_files; do node --check "$f" 2>&1 | sed 's/^/ /' || true; done |
| 88 | else |
| 89 | ok "node --check (every web/static ES module parses)" |
| 90 | fi |
| 91 | else |
| 92 | skip "node not installed" |
| 93 | fi |
| 94 | if [ -f package.json ] && command -v npm >/dev/null 2>&1; then |
| 95 | run "npm test (crypto, ui, format, event vectors)" npm test --silent |
| 96 | else |
| 97 | skip "npm test (no npm or package.json)" |
| 98 | fi |
| 99 | } |
| 100 | |
| 101 | validate_shell() { |
| 102 | section "Shell" |
| 103 | # These scripts have no test suite, so a parse check is the floor. deploy/ |
| 104 | # deliberately mixes sh and bash (ops and its verbs are POSIX sh; the |
| 105 | # collectors are bash and use arrays and `local`), so each file is checked |
| 106 | # with the interpreter its OWN shebang names β forcing `sh -n` on a bash |
| 107 | # script reports syntax errors that are not there. |
| 108 | bad_files='' |
| 109 | for f in "$here"/ops "$here"/*.sh; do |
| 110 | [ -f "$f" ] || continue |
| 111 | case "$(head -1 "$f")" in |
| 112 | *bash) shell='bash' ;; |
| 113 | *) shell='sh' ;; |
| 114 | esac |
| 115 | "$shell" -n "$f" >/dev/null 2>&1 || bad_files="$bad_files $(basename "$f")" |
| 116 | done |
| 117 | if [ -n "$bad_files" ]; then |
| 118 | bad "parse β$bad_files" |
| 119 | for f in $bad_files; do |
| 120 | case "$(head -1 "$here/$f")" in *bash) shell='bash' ;; *) shell='sh' ;; esac |
| 121 | "$shell" -n "$here/$f" 2>&1 | sed 's/^/ /' || true |
| 122 | done |
| 123 | else |
| 124 | ok "parse (every deploy/ script, per its own shebang)" |
| 125 | fi |
| 126 | |
| 127 | if command -v shellcheck >/dev/null 2>&1; then |
| 128 | # Advisory, and shebang-driven (no -s override, same reason as above). The |
| 129 | # tree carries known pre-existing findings (SC1007/SC1091/SC2016), so |
| 130 | # counting them would make this verb useless on day one: it reports and |
| 131 | # never fails. Read it when you touch a script. |
| 132 | if shellcheck "$here"/ops "$here"/*.sh >/dev/null 2>&1; then |
| 133 | ok "shellcheck" |
| 134 | else |
| 135 | printf ' %swarn%s shellcheck findings (advisory β pre-existing ones exist):\n' "$yellow" "$off" |
| 136 | shellcheck "$here"/ops "$here"/*.sh 2>&1 | grep -E '^In ' | sed "s|$root/||" | head -30 | sed 's/^/ /' || true |
| 137 | fi |
| 138 | else |
| 139 | skip "shellcheck not installed (brew install shellcheck)" |
| 140 | fi |
| 141 | } |
| 142 | |
| 143 | validate_infra() { |
| 144 | section "Infra (dev-mesh tofu)" |
| 145 | stack="$root/infra/dev-mesh" |
| 146 | if [ ! -d "$stack" ]; then skip "no infra/dev-mesh"; return; fi |
| 147 | if ! command -v tofu >/dev/null 2>&1; then skip "tofu not installed"; return; fi |
| 148 | # fmt + validate only: both are offline and read-only. `plan` needs AWS |
| 149 | # credentials and talks to the provider, so it stays in `ops dev plan` where |
| 150 | # the credential handling lives β validate must never need a secret. |
| 151 | run "tofu fmt" tofu -chdir="$stack" fmt -check -diff |
| 152 | if [ -d "$stack/.terraform" ]; then |
| 153 | run "tofu validate" tofu -chdir="$stack" validate |
| 154 | else |
| 155 | skip "tofu validate (not initialised β run: ops dev plan)" |
| 156 | fi |
| 157 | } |
| 158 | |
| 159 | what="${1:-all}" |
| 160 | case "$what" in |
| 161 | all) validate_go; validate_web; validate_shell; validate_infra ;; |
| 162 | go) validate_go ;; |
| 163 | web) validate_web ;; |
| 164 | shell) validate_shell ;; |
| 165 | infra) validate_infra ;; |
| 166 | -h|--help|help) |
| 167 | sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' |
| 168 | exit 0 ;; |
| 169 | *) |
| 170 | echo "ops validate: unknown section '$what' (all|go|web|shell|infra)" >&2 |
| 171 | exit 2 ;; |
| 172 | esac |
| 173 | |
| 174 | printf '\n' |
| 175 | if [ "$failed" -eq 0 ]; then |
| 176 | printf '%s%s%s' "$green" "all checks passed" "$off" |
| 177 | [ "$skipped" -gt 0 ] && printf ' (%d skipped)' "$skipped" |
| 178 | printf '\n' |
| 179 | exit 0 |
| 180 | fi |
| 181 | printf '%s%d section(s) FAILED%s\n' "$red" "$failed" "$off" |
| 182 | exit "$failed" |
| 183 | |