Files
summercms/scripts/check-phase19.5.sh
2026-10-21 02:07:44 +02:00

676 lines
26 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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