diff --git a/scripts/check-phase14.3.sh b/scripts/check-phase14.3.sh new file mode 100755 index 0000000..c2d1eef --- /dev/null +++ b/scripts/check-phase14.3.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# Phase 14.3 gate (one-command app scaffolding). +# +# --generator proves D-12 end to end: `summer new demo` run next to a +# symlinked framework checkout, with no --framework flag and no git +# identity, writes an app that builds, migrates against PostgreSQL 16 from +# its own compose.yaml and serves the starter route with no edits. It ends +# with "Phase 14.3 generator check passed". +# +# Needs Docker, the postgres:16 image (pulled when missing) and free host +# ports 54320 and 8080. Every failure exits non-zero. +# +# Overrides: PHASE143_ROOT (framework checkout), PHASE143_KEEP=1 (keep the +# temporary directory), PHASE143_TIMEOUT (seconds to wait for serve, 60). +set -euo pipefail + +unset FORCE_COLOR + +ROOT="${PHASE143_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +SERVE_TIMEOUT="${PHASE143_TIMEOUT:-60}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +port_free() { + # bash /dev/tcp connects when something listens; free means it fails. + if (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; then + return 1 + fi + return 0 +} + +stage_generator() { + command -v docker >/dev/null || fail "docker is not installed" + docker info >/dev/null 2>&1 || fail "the Docker daemon is not reachable (docker info failed)" + docker compose version >/dev/null 2>&1 || fail "docker compose is not available" + command -v git >/dev/null || fail "git is not installed" + command -v curl >/dev/null || fail "curl is not installed" + port_free 54320 || fail "127.0.0.1:54320 is in use; the generated compose.yaml publishes PostgreSQL there" + port_free 8080 || fail "127.0.0.1:8080 is in use; summer serve listens there" + + # Globals, not locals: the EXIT trap runs after this function returns. + tmp="$(mktemp -d)" + app="$tmp/parent/sm-demo-app" + project="phase143-$$-$(date +%s)" + server_pid="" + + cleanup() { + local rc=$? + if [ -n "$server_pid" ]; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + if [ -f "$app/compose.yaml" ]; then + (cd "$app" && COMPOSE_PROJECT_NAME="$project" docker compose down -v --remove-orphans >/dev/null 2>&1) || true + fi + if [ "${PHASE143_KEEP:-0}" = "1" ]; then + echo "kept $tmp" >&2 + else + chmod -R u+w "$tmp" 2>/dev/null || true + rm -rf "$tmp" + fi + exit "$rc" + } + trap cleanup EXIT + + echo "== build the summer CLI" + mkdir -p "$tmp/bin" "$tmp/parent" "$tmp/home" + go -C "$ROOT" build -o "$tmp/bin/summer" ./cmd/summer + export PATH="$tmp/bin:$PATH" + + # Reuse the module and build caches under the empty HOME below. + GOMODCACHE="$(go env GOMODCACHE)" + GOCACHE="$(go env GOCACHE)" + GOPATH="$(go env GOPATH)" + export GOMODCACHE GOCACHE GOPATH + + echo "== summer new demo (sibling detection, no git identity)" + ln -s "$ROOT" "$tmp/parent/summercms" + ( + cd "$tmp/parent" + env -u GIT_AUTHOR_NAME -u GIT_AUTHOR_EMAIL -u GIT_COMMITTER_NAME -u GIT_COMMITTER_EMAIL \ + HOME="$tmp/home" XDG_CONFIG_HOME="$tmp/home/.config" GIT_CONFIG_NOSYSTEM=1 \ + summer new demo + ) | tee "$tmp/new.out" + grep -q "SummerCMS " "$tmp/new.out" || fail "summer new did not announce the fallback git identity" + grep -q "docker compose up -d --wait" "$tmp/new.out" || fail "next steps lack docker compose up -d --wait" + for step in "summer build" "summer migrate" "summer serve"; do + grep -q "$step" "$tmp/new.out" || fail "next steps lack $step" + done + + echo "== layout checks" + [ -d "$app" ] || fail "sm-demo-app was not created" + grep -q "replace git.golem15.com/golem15/summercms => ../summercms" "$app/go.mod" || + fail "go.mod lacks the relative sibling replace" + [ "$(stat -c %a "$app/.env")" = "600" ] || fail ".env mode is $(stat -c %a "$app/.env"), want 600" + grep -q '^SUMMER_APP__KEY=.\{44\}$' "$app/.env" || fail ".env lacks a 44-character SUMMER_APP__KEY" + [ "$(git -C "$app" rev-list --count HEAD)" = "1" ] || fail "want exactly one commit" + [ "$(git -C "$app" rev-parse --abbrev-ref HEAD)" = "main" ] || fail "want branch main" + [ "$(git -C "$app" log -1 --format='%an <%ae>')" = "SummerCMS " ] || + fail "first commit author is not the fallback identity" + [ -z "$(git -C "$app" ls-files .env)" ] || fail ".env is committed" + for f in .env.example go.work main.go plugins.gen.go .gitignore compose.yaml CLAUDE.md AGENTS.md summer.yaml go.mod; do + [ -n "$(git -C "$app" ls-files "$f")" ] || fail "$f is not committed" + done + [ -L "$app/AGENTS.md" ] && [ "$(readlink "$app/AGENTS.md")" = "CLAUDE.md" ] || fail "AGENTS.md is not a symlink to CLAUDE.md" + [ ! -e "$app/go.sum" ] || fail "the app root has a go.sum" + if grep -Eq '^[[:space:]]*/' "$app/go.work"; then + fail "go.work has an absolute use path" + fi + git -C "$app" log -1 --format=%B | grep -qi "co-authored-by" && fail "first commit carries a trailer" + + echo "== docker compose up -d --wait, summer build, summer migrate" + cd "$app" + export COMPOSE_PROJECT_NAME="$project" + docker compose up -d --wait + summer build + summer migrate + + echo "== summer serve" + summer serve >"$tmp/serve.log" 2>&1 & + server_pid=$! + local body="" i + for ((i = 0; i < SERVE_TIMEOUT; i++)); do + if body="$(curl -fsS http://127.0.0.1:8080/ 2>/dev/null)"; then + break + fi + kill -0 "$server_pid" 2>/dev/null || { cat "$tmp/serve.log" >&2; fail "summer serve exited"; } + sleep 1 + done + [ -n "$body" ] || { cat "$tmp/serve.log" >&2; fail "GET / did not answer within ${SERVE_TIMEOUT}s"; } + echo "GET / -> $body" + case "$body" in + *'"plugin":"golem15.demo"'*) ;; + *) fail "GET / body lacks \"plugin\":\"golem15.demo\"" ;; + esac + + echo "Phase 14.3 generator check passed" +} + +case "${1:-}" in +--generator) stage_generator ;; +*) + echo "usage: $0 --generator" >&2 + exit 2 + ;; +esac