#!/usr/bin/env bash # check-phase19.5: fail-closed gate for Phase 19.5, social sign-in in a # shared plugin. It proves the phase across six repositories: this framework # (cooler.Cache.Pull, the decision note, the docs), the shared user plugin # (its additive social sign-in hooks), the social login plugin, the demo # application, the student application (Go and frontend) and the # application whose workspace carries the user plugin checkout. # # Stages: # go go vet ./... and go test -short ./... here, then the # Docker-free cooler tests by required name. # userplugin go vet and go test -short of the user plugin (through the # application workspace), its Docker-free tests by required # name, and the additive check: since PHASE195_USER_BASE no # line declaring an exported func, method or type was removed # from a non-test .go file, and no file under updates/ changed # (D-22). # sociallogin go vet and go test -short in the social login plugin, its # Docker-free tests by required name, no gorilla/sessions and # no gopkg.in/yaml.v3 in `go list -deps .`, and no Go file # importing goth outside classes/providers_goth.go and its # tests. # demo the demo application: go build ./..., go vet ., go test # -short . with its social login tests by required name. # bm the student application: go build ./..., go vet ., go test # -short . and of plugins/jz/bm by required name, then the # frontend's vitest run (oauth.test.ts must pass) and its # typecheck. # fonoteka the application workspace that carries the user plugin: # go vet ./... and go test -short ./..., and its summer.yaml # does not mount golem15.sociallogin. # docker Docker host only: the Postgres and Redis tests by required # name (framework, user plugin, social login plugin), without # -short. The application's unrelated admin route inventory # test is not part of this phase and is not run here # (deferred-items.md). # docs the decision note names markbates/goth and v1.82.0, # TestDocsTree, summer docs:build --check, and no framework # source, README or docs page under modules/ or docs/ names a # consuming application. # # A Go stage exits non-zero on a failing command, or on a go test -json run # that fails, skips a test (a subtest included), matches zero tests, prints # "no tests to run" or misses a required name (a subtest pass never counts # for its parent's name). The vitest detector refuses a run without a # passing oauth.test.ts or with any skipped or failed test. --self-test # proves the detectors on planted output and the docker NOT RUN rule on a # faked PATH without a working docker. # # --all runs self-test, go, userplugin, sociallogin, demo, bm, fonoteka and # docs, then docker when a Docker daemon answers (NOT RUN docker otherwise, # a failure with PHASE195_REQUIRE_DOCKER=1). # # Environment overrides (default: the sibling checkouts next to this # repository): PHASE195_ROOT (this repository), USER_PLUGIN, # SOCIALLOGIN_PLUGIN, DEMO_APP, BM_APP, FONOTEKA (the application workspace # that carries the user plugin), PHASE195_USER_BASE (the user plugin commit # the additive check compares against), PHASE195_REQUIRE_DOCKER. set -euo pipefail unset FORCE_COLOR export NO_COLOR=1 ROOT="${PHASE195_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" FONOTEKA="${FONOTEKA:-$ROOT/../fono""teka.go}" USER_PLUGIN="${USER_PLUGIN:-$FONOTEKA/plugins/golem15/user}" SOCIALLOGIN_PLUGIN="${SOCIALLOGIN_PLUGIN:-$ROOT/../sm-sociallogin-plugin}" DEMO_APP="${DEMO_APP:-$ROOT/../sm-demo-app}" BM_APP="${BM_APP:-$ROOT/../sm-bm-app}" # The user plugin commit the phase started from (PATTERNS.md). USER_BASE="${PHASE195_USER_BASE:-4c5d3b8}" # --------------------------------------------------------------------------- # Required test names. Each list runs with -run '^(a|b|...)$' and every name # must pass; a name that skips or does not run fails the stage. # go stage: cooler.Cache.Pull (-short, memory and miniredis). COOLER_TESTS=( # Plan 01. TestPullSmoke # Plan 04: concurrency, expiry and prefix isolation (D-08). TestPullConcurrent TestPullExpired TestPullPrefixIsolation ) # userplugin stage: the user plugin's Docker-free tests (-short), root # package, through the application workspace. USER_ROOT_TESTS=( # Plan 01. TestOAuthProvidersSourceSmoke # Plan 04: the provider source (D-15). TestOAuthProviders ) # sociallogin stage: the plugin's Docker-free tests (-short). SOCIAL_TESTS=( # Plan 02. TestGothProvidersSmoke # Plan 04: the Docker-free unit tests (D-03, D-06, D-08, D-11, D-17). TestRegistry TestNormalizeFrontendCallback TestNormalizeReturnTo TestCodes TestEmailVerified TestGothProviders TestErrorMessages # Plan 04: the PHP social-auth test map (38 methods). TestPHPMethodMap ) # demo stage (-short). DEMO_TESTS=(TestBootAllPlugins TestSocialLoginRedirect TestSocialLoginCoexistsWithOAuthServer) # bm stage (-short): the application root and its domain plugin. BM_TESTS=(TestSocialLoginMounted) BM_PLUGIN_TESTS=(TestStaticReservedPaths TestEmbeddedPublicHasIndex) # docker stage: testcontainers suites (Postgres, Redis). DOCKER_COOLER_TESTS=( # Plan 04: Pull on redis:7-alpine (D-08). TestPullDocker ) DOCKER_USER_ROOT_TESTS=( # Plan 01. TestSocialHooksSmoke TestOAuthIdentitiesDestroyConcurrentSmoke TestOAuthIdentitiesDestroyLastIdentityWithPassword TestBackfillHandlesOpensFromConfigWhenUnpublished # Plan 04: the unlink rule and its race (D-13), the sign-in helpers (D-22). TestOAuthIdentitiesDestroyRule TestOAuthIdentitiesDestroyRace TestRegisterUserPasswordLess TestFinishLogin TestRegistrationExports # Plan 04: the PHP map row of OAuthPasswordBootstrapTest's # real-password case. TestChangePassword ) DOCKER_SOCIAL_TESTS=( # Plan 02. TestTracerSmoke TestFlowSmoke TestLinkSmoke TestSettingsOverrideSmoke # Plan 04: one test per decision on Postgres (D-04, D-07, D-09, D-10, # D-11, D-12, D-14, D-15, D-16, D-19, D-20, D-23). TestStateCookie TestRoutes TestRouteMiddleware TestProvidersList TestMatching TestNOAuth TestSoftDeleted TestInactiveAndBanned TestRegistrationGates TestConsent TestBetaGate TestIdentityTokens TestLink TestLinkTicket TestOAuthComplete TestTokenCookieMode TestRegisterComplete TestSettings # Plan 04 security review: no auto-link onto a password-less account. TestPreAccountTakeover # Plan 04: the PHP ports, one subtest per PHP method. TestPHPSocialAuthGate TestPHPOAuthLinkCookie TestPHPPendingRegistrationTakeover TestPHPPasswordBootstrap TestPHPProfileOAuthLink ) # docs stage: names of consuming applications (split so this file does not # match itself), the decision note and the dependency names it must carry. APP_NAMES=("p[lł]yta""rium" "fono""teka" "wave""path" "horo""skopia" "bm\\.jakub""zych") DECISION_NOTE=".planning/notes/2026-10-20-sociallogin-goth.md" DECISION_NAMES=("markbates/goth" "v1.82.0") usage() { cat >&2 <<'EOF' usage: check-phase19.5.sh --self-test check-phase19.5.sh go | userplugin | sociallogin | demo | bm | fonoteka | docker | docs check-phase19.5.sh --go | --userplugin | --sociallogin | --demo | --bm | --fonoteka | --docker | --docs check-phase19.5.sh --all EOF exit 2 } # detect_go reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests, # 4 non-JSON, 5 missing required name. detect_go() { python3 - "$1" <<'PY' import json, os, sys path = sys.argv[1] require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n] passed = set() failed = [] with open(path, encoding="utf-8", errors="replace") as fh: for raw in fh: line = raw.strip() if not line.startswith("{"): continue try: ev = json.loads(line) except json.JSONDecodeError: print("refuse: non-json test output", file=sys.stderr) sys.exit(4) action = ev.get("Action") test = ev.get("Test") or "" pkg = ev.get("Package") or ev.get("ImportPath") or "" if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")): print(f"refuse: build failed {pkg}", file=sys.stderr) sys.exit(1) text = ev.get("Output") or "" if action == "output" and "no tests to run" in text: print(f"refuse: no tests to run in {pkg}", file=sys.stderr) sys.exit(3) if action == "skip" and test: print(f"refuse: skipped {pkg} {test}", file=sys.stderr) sys.exit(2) if action == "fail": failed.append(f"{pkg} {test}".strip()) if action == "pass" and test: passed.add(test) if failed: print("refuse: failed " + ", ".join(failed), file=sys.stderr) sys.exit(1) if not passed: print("refuse: zero tests", file=sys.stderr) sys.exit(3) missing = [n for n in require if n not in passed] if missing: print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) sys.exit(5) PY } # detect_vitest reads a vitest run's output. Exit 1 failed or skipped # tests, 3 no summary, 5 the required file (REQUIRE_FILE) did not pass. detect_vitest() { python3 - "$1" <<'PY' import os, re, sys text = open(sys.argv[1], encoding="utf-8", errors="replace").read() text = re.sub(r"\x1b\[[0-9;]*m", "", text) need = os.environ.get("REQUIRE_FILE", "") if re.search(r"\bfailed\b|\bskipped\b|\btodo\b|\bFAIL\b", text): print("refuse: vitest reports failed, skipped or todo tests", file=sys.stderr) sys.exit(1) m = re.search(r"^\s*Tests\s+(\d+) passed \((\d+)\)\s*$", text, re.M) if not m or m.group(1) != m.group(2) or m.group(1) == "0": print("refuse: no all-passing vitest summary", file=sys.stderr) sys.exit(3) if need and not re.search(r"^\s*✓ " + re.escape(need) + r" \(\d+ tests?\)", text, re.M): print(f"refuse: {need} did not pass", file=sys.stderr) sys.exit(5) PY } # go_json DIR ARGS... runs go test -json ARGS in DIR and checks the stream # with detect_go. go_json() { local dir="$1" shift local log err rc=0 dc=0 log="$(mktemp)" err="$(mktemp)" (cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$? detect_go "$log" || dc=$? if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then cat "$err" >&2 || true grep -v '^{' "$log" | tail -n 40 >&2 || true python3 -c 'import json,sys for raw in open(sys.argv[1], errors="replace"): try: ev = json.loads(raw) except ValueError: continue if ev.get("Action") == "output" and ("FAIL" in ev.get("Output", "") or "SKIP" in ev.get("Output", "") or "_test.go:" in ev.get("Output", "")): sys.stderr.write(ev["Output"])' "$log" 2>&1 | tail -n 40 >&2 || true rm -f "$log" "$err" echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 return 1 fi echo "go: $(grep -c '"Action":"pass","Package":"[^"]*","Test":"[^"/]*","Elapsed"' "$log") top-level tests passed in $dir ($*)" rm -f "$log" "$err" } # go_named DIR PACKAGES FLAGS NAME... runs exactly the named top-level tests # and requires every one of them to pass. PACKAGES and FLAGS are word-split. go_named() { local dir="$1" pkgs="$2" flags="$3" shift 3 if [[ "$#" -eq 0 ]]; then echo "refuse: go_named $pkgs in $dir without required names" >&2 return 1 fi local names=("$@") regex regex="^($(IFS='|'; echo "${names[*]}"))\$" # shellcheck disable=SC2086 REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" $pkgs } # go_short DIR PACKAGES runs go test -short PACKAGES in DIR; on failure it # prints the tail of the output and fails. go_short() { local dir="$1" pkgs="${2:-./...}" out # shellcheck disable=SC2086 if ! out="$(cd "$dir" && go test -count=1 -short $pkgs 2>&1)"; then printf '%s\n' "$out" | grep -v '^ok ' | tail -n 60 >&2 echo "refuse: go test -short $pkgs in $dir" >&2 return 1 fi echo "short: $dir ($pkgs)" } go_vet() { local dir="$1" shift (cd "$dir" && go vet "$@") echo "vet: $dir ($*)" } go_build() { local dir="$1" shift (cd "$dir" && go build "$@") echo "build: $dir ($*)" } require_dir() { [[ -d "$1" ]] || { echo "refuse: $2 $1 not found" >&2 return 1 } } expect_detector() { local detector="$1" name="$2" want="$3" payload="$4" log dc=0 log="$(mktemp)" printf '%s\n' "$payload" >"$log" "$detector" "$log" >/dev/null 2>&1 || dc=$? rm -f "$log" if [[ "$dc" -ne "$want" ]]; then echo "refuse: self-test $detector $name: exit $dc, want $want" >&2 return 1 fi } # expect_mode NAME WANT_EXIT WANT_TEXT MODE ENV... runs this script in MODE # with ENV and checks its exit status and that its output carries WANT_TEXT. expect_mode() { local name="$1" want="$2" text="$3" mode="$4" shift 4 local out rc=0 out="$(env "$@" bash "${BASH_SOURCE[0]}" "$mode" 2>&1)" || rc=$? if [[ "$rc" -ne "$want" ]] || ! grep -qF -- "$text" <<<"$out"; then printf '%s\n' "$out" | tail -n 10 >&2 echo "refuse: self-test $name: exit $rc (want $want), output must carry \"$text\"" >&2 return 1 fi } # removed_exports reads a unified diff and prints every removed line that # declares an exported func, method or type. removed_exports() { grep -E '^-(func ([A-Z]|\([^)]*\) [A-Z])|type [A-Z])' | grep -vE '^--- ' || true } # forbidden_deps reads `go list -deps` output and prints every forbidden # module package: gorilla/sessions (goth's session helper) and the archived # gopkg.in/yaml.v3. forbidden_deps() { grep -E '^(github\.com/gorilla/sessions|gopkg\.in/yaml\.v3)(/|$)' || true } run_self_test() { bash -n "${BASH_SOURCE[0]}" expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestTracerSmoke"}' expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p","Test":"TestNOAuth"}' expect_detector detect_go package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p"}' expect_detector detect_go build 1 '{"Action":"build-fail","ImportPath":"p"}' expect_detector detect_go skip 2 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"skip","Package":"p","Test":"TestFlowSmoke"}' expect_detector detect_go subtest-skip 2 '{"Action":"skip","Package":"p","Test":"TestStateCookie/tampered"} {"Action":"pass","Package":"p","Test":"TestStateCookie"}' expect_detector detect_go zero 3 '{"Action":"pass","Package":"p"}' expect_detector detect_go no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"} {"Action":"pass","Package":"p"}' expect_detector detect_go nonjson 4 '{"Action":"pass",' REQUIRE_TESTS="TestTracerSmoke TestPHPMethodMap" expect_detector detect_go missing-named 5 \ '{"Action":"pass","Package":"p","Test":"TestTracerSmoke"}' REQUIRE_TESTS="TestLink" expect_detector detect_go prefix-is-not-the-test 5 \ '{"Action":"pass","Package":"p","Test":"TestLinkTicket"}' REQUIRE_TESTS="TestLink" expect_detector detect_go subtest-is-not-the-test 5 \ '{"Action":"pass","Package":"p","Test":"TestOuter/TestLink"} {"Action":"pass","Package":"p","Test":"TestOther"}' local vitest_ok vitest_skip vitest_fail vitest_missing vitest_ok=$' ✓ src/lib/oauth.test.ts (10 tests) 14ms\n ✓ src/pwa/pwa.test.ts (11 tests) 17ms\n\n Test Files 2 passed (2)\n Tests 21 passed (21)' vitest_skip=$' ✓ src/lib/oauth.test.ts (10 tests | 1 skipped) 14ms\n\n Test Files 1 passed (1)\n Tests 9 passed | 1 skipped (10)' vitest_fail=$' ❯ src/lib/oauth.test.ts (10 tests | 1 failed) 14ms\n\n Test Files 1 failed (1)\n Tests 1 failed | 9 passed (10)' vitest_missing=$' ✓ src/pwa/pwa.test.ts (11 tests) 17ms\n\n Test Files 1 passed (1)\n Tests 11 passed (11)' REQUIRE_FILE="src/lib/oauth.test.ts" expect_detector detect_vitest pass 0 "$vitest_ok" REQUIRE_FILE="src/lib/oauth.test.ts" expect_detector detect_vitest skip 1 "$vitest_skip" REQUIRE_FILE="src/lib/oauth.test.ts" expect_detector detect_vitest fail 1 "$vitest_fail" REQUIRE_FILE="src/lib/oauth.test.ts" expect_detector detect_vitest missing-file 5 "$vitest_missing" REQUIRE_FILE="src/lib/oauth.test.ts" expect_detector detect_vitest no-summary 3 ' ✓ src/lib/oauth.test.ts (10 tests) 14ms' # The NOT RUN rule, on a faked host: a docker binary that never answers. local tmp tmp="$(mktemp -d)" mkdir -p "$tmp/bin" printf '#!/bin/sh\nexit 1\n' >"$tmp/bin/docker" chmod +x "$tmp/bin/docker" expect_mode docker-not-run 0 "NOT RUN docker" --docker-gate PATH="$tmp/bin:$PATH" PHASE195_REQUIRE_DOCKER=0 expect_mode docker-required 1 "FAIL docker" --docker-gate PATH="$tmp/bin:$PATH" PHASE195_REQUIRE_DOCKER=1 expect_mode docker-stage-needs-daemon 1 "needs a reachable Docker daemon" --docker PATH="$tmp/bin:$PATH" rm -rf "$tmp" # The additive check refuses a removed export and accepts additions. local diff_bad diff_ok line diff_bad=$'--- a/controllers/x.go\n+++ b/controllers/x.go\n-func FinishLogin(ctx context.Context) error {\n+func finishLogin(ctx context.Context) error {' diff_ok=$'--- a/controllers/x.go\n+++ b/controllers/x.go\n+func RefreshTTL(app *backpack.App) time.Duration {\n-func helper() {}\n-\t// func Old() is gone' if [[ -z "$(removed_exports <<<"$diff_bad")" ]]; then echo "refuse: self-test removed_exports missed a removed exported func" >&2 return 1 fi if [[ -n "$(removed_exports <<<"$diff_ok")" ]]; then echo "refuse: self-test removed_exports refused an additive diff" >&2 return 1 fi for line in '-type OAuthProviderSource interface {' '-func (u *User) AvatarURL(size int) string {' '-func (User) Name() string {'; do if [[ -z "$(removed_exports <<<"$line")" ]]; then echo "refuse: self-test removed_exports missed: $line" >&2 return 1 fi done # The dependency check refuses gorilla/sessions and gopkg.in/yaml.v3, # and accepts their neighbours. if [[ -z "$(forbidden_deps <<<$'net/http\ngithub.com/gorilla/sessions')" ]] || [[ -z "$(forbidden_deps <<<'gopkg.in/yaml.v3')" ]]; then echo "refuse: self-test forbidden_deps missed a forbidden package" >&2 return 1 fi if [[ -n "$(forbidden_deps <<<$'go.yaml.in/yaml/v3\ngithub.com/gorilla/css/scanner\ngithub.com/markbates/goth')" ]]; then echo "refuse: self-test forbidden_deps refused an allowed package" >&2 return 1 fi local mode for mode in self-test go userplugin sociallogin demo bm fonoteka docker docs all; do grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || { echo "refuse: missing mode --$mode" >&2 return 1 } done echo "phase19.5 self-test passed" } run_go() { go_vet "$ROOT" ./... go_short "$ROOT" go_named "$ROOT" ./modules/cooler -short "${COOLER_TESTS[@]}" echo "phase19.5 go passed" } run_userplugin() { require_dir "$USER_PLUGIN" "user plugin" require_dir "$FONOTEKA" "application workspace" go_vet "$FONOTEKA" ./plugins/golem15/user/... go_short "$FONOTEKA" ./plugins/golem15/user/... go_named "$FONOTEKA" ./plugins/golem15/user -short "${USER_ROOT_TESTS[@]}" # D-22: additive only. git's status is captured before any filtering, so # an unknown base or a broken checkout fails here instead of reading as # an empty diff. local diff rc=0 removed added migrations diff="$(git -C "$USER_PLUGIN" diff "$USER_BASE" -- '*.go' ':(exclude)*_test.go' 2>&1)" || rc=$? if [[ "$rc" -ne 0 ]]; then printf '%s\n' "$diff" | tail -n 5 >&2 echo "refuse: git diff $USER_BASE in $USER_PLUGIN failed (exit $rc)" >&2 return 1 fi removed="$(removed_exports <<<"$diff")" if [[ -n "$removed" ]]; then printf '%s\n' "$removed" >&2 echo "refuse: the user plugin removed or changed exported declarations since $USER_BASE (D-22: additive only)" >&2 return 1 fi added="$(grep -cE '^\+(func|type) ' <<<"$diff" || true)" rc=0 migrations="$(git -C "$USER_PLUGIN" diff --name-only "$USER_BASE" -- updates/ 2>&1)" || rc=$? if [[ "$rc" -ne 0 ]]; then printf '%s\n' "$migrations" | tail -n 5 >&2 echo "refuse: git diff --name-only $USER_BASE -- updates/ in $USER_PLUGIN failed (exit $rc)" >&2 return 1 fi if [[ -n "$migrations" ]]; then printf '%s\n' "$migrations" >&2 echo "refuse: the user plugin changed files under updates/ since $USER_BASE (D-22: no migration change)" >&2 return 1 fi echo "additive: no exported func, method or type removed and no migration changed in the user plugin since $USER_BASE ($added declarations added)" echo "phase19.5 userplugin passed" } run_sociallogin() { require_dir "$SOCIALLOGIN_PLUGIN" "social login plugin" go_vet "$SOCIALLOGIN_PLUGIN" ./... # The Postgres suites skip here and run by name in the docker stage. go_short "$SOCIALLOGIN_PLUGIN" USER_PLUGIN="$USER_PLUGIN" go_named "$SOCIALLOGIN_PLUGIN" . -short "${SOCIAL_TESTS[@]}" local deps rc=0 bad importers deps="$(cd "$SOCIALLOGIN_PLUGIN" && go list -deps . 2>&1)" || rc=$? if [[ "$rc" -ne 0 ]]; then printf '%s\n' "$deps" | tail -n 5 >&2 echo "refuse: go list -deps . in $SOCIALLOGIN_PLUGIN failed (exit $rc)" >&2 return 1 fi bad="$(forbidden_deps <<<"$deps")" if [[ -n "$bad" ]]; then printf '%s\n' "$bad" >&2 echo "refuse: the social login plugin links gorilla/sessions or gopkg.in/yaml.v3" >&2 return 1 fi importers="$(cd "$SOCIALLOGIN_PLUGIN" && grep -rlE '"github\.com/markbates/goth(/[^"]*)?"' --include='*.go' . | grep -vE '^\./classes/providers_goth(_[a-z_]+)?(_test)?\.go$' || true)" if [[ -n "$importers" ]]; then printf '%s\n' "$importers" >&2 echo "refuse: goth is imported outside classes/providers_goth.go and its tests" >&2 return 1 fi echo "deps: no gorilla/sessions, no gopkg.in/yaml.v3; goth only in classes/providers_goth.go" echo "phase19.5 sociallogin passed" } run_demo() { require_dir "$DEMO_APP" "demo application" go_build "$DEMO_APP" ./... go_vet "$DEMO_APP" . go_named "$DEMO_APP" . -short "${DEMO_TESTS[@]}" echo "phase19.5 demo passed" } run_bm() { require_dir "$BM_APP" "student application" go_build "$BM_APP" ./... go_vet "$BM_APP" . go_named "$BM_APP" . -short "${BM_TESTS[@]}" go_named "$BM_APP" ./plugins/jz/bm -short "${BM_PLUGIN_TESTS[@]}" local log rc=0 dc=0 log="$(mktemp)" npm --prefix "$BM_APP/frontend" run test >"$log" 2>&1 || rc=$? REQUIRE_FILE="src/lib/oauth.test.ts" detect_vitest "$log" || dc=$? if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then tail -n 40 "$log" >&2 rm -f "$log" echo "refuse: npm run test in $BM_APP/frontend (test=$rc detect=$dc)" >&2 return 1 fi echo "vitest: $(grep -E '^\s*Tests\s' "$log" | tr -s ' ')" rm -f "$log" rc=0 log="$(mktemp)" npm --prefix "$BM_APP/frontend" run typecheck >"$log" 2>&1 || rc=$? if [[ "$rc" -ne 0 ]]; then tail -n 40 "$log" >&2 rm -f "$log" echo "refuse: npm run typecheck in $BM_APP/frontend" >&2 return 1 fi rm -f "$log" echo "typecheck: $BM_APP/frontend" echo "phase19.5 bm passed" } run_fonoteka() { require_dir "$FONOTEKA" "application workspace" go_vet "$FONOTEKA" ./... go_short "$FONOTEKA" ./... if grep -qE '(^|[^a-z.])golem15\.sociallogin([^a-z.]|$)' "$FONOTEKA/summer.yaml"; then grep -nE 'golem15\.sociallogin' "$FONOTEKA/summer.yaml" >&2 echo "refuse: $FONOTEKA/summer.yaml mounts golem15.sociallogin" >&2 return 1 fi echo "mount: $FONOTEKA/summer.yaml does not mount golem15.sociallogin" echo "phase19.5 fonoteka passed" } docker_up() { command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 } run_docker() { if ! docker_up; then echo "refuse: the docker stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2 return 1 fi require_dir "$SOCIALLOGIN_PLUGIN" "social login plugin" require_dir "$FONOTEKA" "application workspace" if ((${#DOCKER_COOLER_TESTS[@]})); then go_named "$ROOT" ./modules/cooler "-timeout 10m" "${DOCKER_COOLER_TESTS[@]}" fi go_named "$FONOTEKA" ./plugins/golem15/user "-timeout 20m" "${DOCKER_USER_ROOT_TESTS[@]}" USER_PLUGIN="$USER_PLUGIN" go_named "$SOCIALLOGIN_PLUGIN" . "-timeout 30m" "${DOCKER_SOCIAL_TESTS[@]}" echo "phase19.5 docker passed" } # docker_gate is the docker step of --all: run the stage when a daemon # answers, fail with PHASE195_REQUIRE_DOCKER=1, print NOT RUN otherwise. docker_gate() { if docker_up; then if bash "${BASH_SOURCE[0]}" --docker; then echo "PASS docker" return 0 fi echo "FAIL docker" return 1 fi if [[ "${PHASE195_REQUIRE_DOCKER:-0}" == "1" ]]; then echo "FAIL docker: no reachable Docker daemon and PHASE195_REQUIRE_DOCKER=1" return 1 fi cat >&2 <<'EOF' ************************************************************************ NOT RUN docker: no reachable Docker daemon on this host. The testcontainers suites (Postgres, Redis) of the framework, the user plugin and the social login plugin did NOT run. Run on a Docker host (through sudo -u golem when the socket is denied) before /msd-verify-work: bash scripts/check-phase19.5.sh docker ************************************************************************ EOF echo "NOT RUN docker (Docker-host only; see the notice above)" } run_docs() { local name for name in "${DECISION_NAMES[@]}"; do if ! grep -qF -- "$name" "$ROOT/$DECISION_NOTE"; then echo "refuse: the decision note $DECISION_NOTE does not name $name" >&2 return 1 fi done echo "note: $DECISION_NOTE names ${DECISION_NAMES[*]}" go_named "$ROOT" ./cmd/summer "" TestDocsTree (cd "$ROOT" && go run ./cmd/summer docs:build --check) local pattern hits files=() pattern="$(IFS='|'; echo "${APP_NAMES[*]}")" while IFS= read -r -d '' f; do files+=("$f") done < <(find "$ROOT/modules" "$ROOT/docs" -type f ! -name '*_test.go' ! -path '*/node_modules/*' -print0) hits="$(grep -inHIE "$pattern" "${files[@]}" || true)" if [[ -n "$hits" ]]; then printf '%s\n' "$hits" | head -n 20 >&2 echo "refuse: a framework source, README or docs page names a consuming application" >&2 return 1 fi echo "names: ${#files[@]} files under modules/ and docs/ checked, no consuming application named" echo "phase19.5 docs passed" } run_all() { local stage for stage in self-test go userplugin sociallogin demo bm fonoteka docs; do if bash "${BASH_SOURCE[0]}" "--$stage"; then echo "PASS $stage" else echo "FAIL $stage" exit 1 fi done docker_gate || exit 1 echo "Phase 19.5 gate passed" } case "${1:---all}" in --self-test) run_self_test ;; --go) run_go ;; --userplugin) run_userplugin ;; --sociallogin) run_sociallogin ;; --demo) run_demo ;; --bm) run_bm ;; --fonoteka) run_fonoteka ;; --docker) run_docker ;; --docker-gate) docker_gate ;; --docs) run_docs ;; --all) run_all ;; self-test) run_self_test ;; go) run_go ;; userplugin) run_userplugin ;; sociallogin) run_sociallogin ;; demo) run_demo ;; bm) run_bm ;; fonoteka) run_fonoteka ;; docker) run_docker ;; docs) run_docs ;; all) run_all ;; *) usage ;; esac