#!/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