- new.sh gains --update, run inside one application directory (go.mod and summer.yaml required); it changes only that application - the framework checkout is read from the app's go.mod replace (single-line or block form); module-version replaces, missing targets and foreign or dirty checkouts are refused through the shared check_framework_dir - a detached checkout moves fast-forward only to the newest release (or SUMMERCMS_REF) through the shared move_detached; a branch checkout is pulled with git pull --no-rebase --ff-only, refusing a missing upstream or a diverged branch with the checkout unchanged - other sm-*-app directories on the same checkout are named on one "not rebuilt" line and never built or written - summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to this app's .env (openssl, else head and base64; mode kept, never printed), then summer build runs and the script ends with the migrate and restart reminder and the shared path_hint - sunrise.UpdateScript() serves the same script with its last line changed to main "$@" --update, so truncated downloads still run nothing - check-phase14.3.sh --forbidden now allows only the literal SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other SUMMER_ name or on a read or export of one; TestUpdateScript and TestUpdateScriptRoute are required tests
579 lines
22 KiB
Bash
Executable File
579 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Phase 14.3 gate (one-command app scaffolding).
|
|
#
|
|
# --all is the authoritative, fail-closed phase gate. It runs each stage
|
|
# below in its own process and prints the final gate marker only after
|
|
# all of --unit, --generator, --bootstrap, --terminal, --acceptance and
|
|
# --security pass.
|
|
#
|
|
# --unit runs go vet ./..., the full framework test suite (-count=1 -v, never
|
|
# -short or -update) and requires a "--- PASS:" line for every test in
|
|
# REQUIRED_TESTS; a skipped required test, "no tests to run" or a golden file
|
|
# changed by the run fails the stage. It also runs the examples/hello tests,
|
|
# docs:build --check, the site plugin and app tests, the landing page
|
|
# generate and tests, and --forbidden.
|
|
#
|
|
# --forbidden scans for surfaces the phase must not add: a framework config
|
|
# variable (SUMMER_ prefix) in new.sh other than SUMMER_ADMIN__JWT__SECRET,
|
|
# which update mode appends to an application's .env, or any read
|
|
# ($SUMMER_..., ${SUMMER_...}) or export of a SUMMER_ variable in new.sh
|
|
# (D-26: the script's own overrides are SUMMERCMS_*), a new require in go.mod
|
|
# since v0.1.12,
|
|
# the stdlib plugin package or yaegi in the dependency graph, an absolute
|
|
# path in a golden go.mod/go.work, or a golden ignored by .gitignore.
|
|
#
|
|
# --bootstrap runs the gated TestBootstrapEndToEnd; --terminal runs the
|
|
# site's gated TestTerminalCommands against the committed new.sh and this
|
|
# checkout at HEAD (SUMMERCMS_NEW_URL/REPO/REF overrides). Each requires its
|
|
# named PASS.
|
|
#
|
|
# --security requires every threat T-14.3-01..26 and T-14.3-SC in
|
|
# 14.3-SECURITY-REVIEW.md, no high row with status open, and a named test or
|
|
# gate stage in the "Pinned by" column of every high row.
|
|
#
|
|
# --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".
|
|
#
|
|
# --acceptance proves D-20 before the release tag exists: in a bare
|
|
# node:24-bookworm container it installs jq, the Docker CLI and compose
|
|
# plugin from Docker's signed apt repository and Go 1.27.0 from go.dev
|
|
# (sha256 checked), pipes the committed modules/sunrise/new.sh into
|
|
# `bash -s -- demo` with SUMMERCMS_REPO=<this checkout> and SUMMERCMS_REF=HEAD,
|
|
# then runs `docker compose up -d --wait && summer build && summer migrate &&
|
|
# summer serve` in sm-demo-app and requires the starter route. The container
|
|
# drives the host Docker daemon through its socket, so it runs with
|
|
# --network host under a unique compose project that is removed afterwards.
|
|
# It ends with "Phase 14.3 acceptance passed".
|
|
#
|
|
# --acceptance-production runs the same chain with
|
|
# `curl -fsSL https://summercms.io/new | bash -s -- demo` and no SUMMERCMS_
|
|
# overrides, after the release is tagged and the site redeployed. It ends
|
|
# with "Phase 14.3 production acceptance 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}"
|
|
SITE="${PHASE143_SITE:-$ROOT/../sm-summercmsio-app}"
|
|
PHASE_DIR="${PHASE143_PHASE_DIR:-$ROOT/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new}"
|
|
REVIEW="$PHASE_DIR/14.3-SECURITY-REVIEW.md"
|
|
|
|
# Framework tests that must each print "--- PASS:" in the --unit run.
|
|
REQUIRED_TESTS=(
|
|
TestNewGolden
|
|
TestNewGoldenNoPlugin
|
|
TestNewAppSmoke
|
|
TestNewNoPluginBuilds
|
|
TestNewValidateAppName
|
|
TestNewRefusesExistingTarget
|
|
TestNewFrameworkResolution
|
|
TestNewRefusesTargetInsideFramework
|
|
TestNewCleansUpOnFailure
|
|
TestNewRequiresValidAppKey
|
|
TestNewEnvFileMode
|
|
TestNewAgentsSymlink
|
|
TestNewGitFirstCommit
|
|
TestNewGitUsesConfiguredIdentity
|
|
TestNewGitMissingIsWarning
|
|
TestNewNoGit
|
|
TestMakeAndAddPluginOnHelloCopy
|
|
TestMakePluginRoutesUnchanged
|
|
TestGenerateAppKey
|
|
TestNewCommandViaCLI
|
|
TestToolCommandNames
|
|
TestScriptEmbedded
|
|
TestScriptShape
|
|
TestScriptTruncationRunsNothing
|
|
TestScriptBehavior
|
|
TestScriptShellcheck
|
|
TestUpdateScript
|
|
TestDocsTree
|
|
TestDocsAIOutputsInSync
|
|
)
|
|
# Site tests that pin GET /new and the landing page commands.
|
|
SITE_PLUGIN_TESTS=(TestNewScriptRoute TestUpdateScriptRoute TestRoutesAssemble)
|
|
SITE_APP_TESTS=(TestLoadTerminal TestTerminalScript TestTerminalEnv TestTerminalRunScript)
|
|
|
|
fail() {
|
|
echo "FAIL: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# require_named_pass LOG NAME...: the go test -v log must contain no
|
|
# "no tests to run", no failure, no skip of a named test and a top-level
|
|
# "--- PASS: NAME (" line for every name.
|
|
require_named_pass() {
|
|
local log="$1" name
|
|
shift
|
|
if grep -q 'no tests to run' "$log"; then
|
|
fail "$log: a test pattern matched no tests"
|
|
fi
|
|
if grep -qE '^[[:space:]]*--- FAIL|^FAIL' "$log"; then
|
|
grep -E '^[[:space:]]*--- FAIL|^FAIL' "$log" >&2
|
|
fail "$log: a test failed"
|
|
fi
|
|
for name in "$@"; do
|
|
if grep -qE "^[[:space:]]*--- SKIP: ${name}( |/|\$)" "$log"; then
|
|
fail "required test $name was skipped"
|
|
fi
|
|
grep -qE "^--- PASS: ${name} \(" "$log" || fail "required test $name has no --- PASS line"
|
|
echo "PASS $name"
|
|
done
|
|
}
|
|
|
|
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 <summer@localhost>" "$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 <summer@localhost>" ] ||
|
|
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"
|
|
}
|
|
|
|
# acceptance_inner runs inside node:24-bookworm as root. ACC_MODE is local
|
|
# or production; ACC_REF is the framework commit for local runs.
|
|
read -r -d '' acceptance_inner <<'INNER' || true
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
run() {
|
|
echo "+ $*"
|
|
"$@"
|
|
}
|
|
|
|
echo "== apt: ca-certificates curl gnupg jq"
|
|
apt-get update -qq
|
|
apt-get install -y -qq ca-certificates curl gnupg jq >/dev/null
|
|
|
|
echo "== Docker CLI and compose plugin from Docker's apt repository"
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" >/etc/apt/sources.list.d/docker.list
|
|
apt-get update -qq
|
|
apt-get install -y -qq docker-ce-cli docker-compose-plugin >/dev/null
|
|
docker --version
|
|
docker compose version
|
|
|
|
echo "== Go 1.27.0 from go.dev, sha256 checked"
|
|
tarball=go1.27.0.linux-amd64.tar.gz
|
|
sha="$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' |
|
|
jq -r --arg f "$tarball" '.[] | select(.version == "go1.27.0") | .files[] | select(.filename == $f) | .sha256')"
|
|
[[ $sha =~ ^[0-9a-f]{64}$ ]] || { echo "no sha256 for $tarball on go.dev" >&2; exit 1; }
|
|
curl -fsSL -o "/tmp/$tarball" "https://go.dev/dl/$tarball"
|
|
(cd /tmp && echo "$sha $tarball" | sha256sum -c -)
|
|
tar -C /usr/local -xzf "/tmp/$tarball"
|
|
export PATH="/usr/local/go/bin:$PATH"
|
|
go version
|
|
|
|
# Throwaway root container cloning a host-owned checkout.
|
|
git config --global --add safe.directory '*'
|
|
|
|
mkdir /work
|
|
cd /work
|
|
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 /work/sm-demo-app/compose.yaml ]; then
|
|
(cd /work/sm-demo-app && docker compose down -v --remove-orphans >/dev/null 2>&1) || true
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if [ "$ACC_MODE" = production ]; then
|
|
echo "+ curl -fsSL https://summercms.io/new | bash -s -- demo"
|
|
curl -fsSL https://summercms.io/new | bash -s -- demo 2>&1 | tee /tmp/new.out
|
|
else
|
|
echo "+ curl -fsSL file:///fw/modules/sunrise/new.sh | SUMMERCMS_REPO=/fw SUMMERCMS_REF=$ACC_REF bash -s -- demo"
|
|
curl -fsSL file:///fw/modules/sunrise/new.sh | SUMMERCMS_REPO=/fw SUMMERCMS_REF="$ACC_REF" bash -s -- demo 2>&1 | tee /tmp/new.out
|
|
fi
|
|
grep -qx ' cd /work/sm-demo-app' /tmp/new.out || { echo "the script did not print cd /work/sm-demo-app" >&2; exit 1; }
|
|
grep -qF 'export PATH="/root/go/bin:$PATH"' /tmp/new.out || { echo "the script did not print the PATH hint" >&2; exit 1; }
|
|
export PATH="$(go env GOPATH)/bin:$PATH"
|
|
|
|
echo "== D-20 chain"
|
|
cd sm-demo-app
|
|
run docker compose up -d --wait
|
|
run summer build
|
|
run summer migrate
|
|
echo "+ summer serve &"
|
|
summer serve >/tmp/serve.log 2>&1 &
|
|
server_pid=$!
|
|
body=""
|
|
for ((i = 0; i < 60; 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; echo "summer serve exited" >&2; exit 1; }
|
|
sleep 1
|
|
done
|
|
[ -n "$body" ] || { cat /tmp/serve.log >&2; echo "GET / did not answer within 60s" >&2; exit 1; }
|
|
echo "GET http://127.0.0.1:8080/ -> $body"
|
|
case "$body" in
|
|
*'"plugin":"golem15.demo"'*) ;;
|
|
*) echo "GET / body lacks \"plugin\":\"golem15.demo\"" >&2; exit 1 ;;
|
|
esac
|
|
INNER
|
|
|
|
# stage_acceptance runs acceptance_inner in node:24-bookworm. $1 is local
|
|
# (committed script and repository at HEAD) or production.
|
|
stage_acceptance() {
|
|
local mode="$1"
|
|
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)"
|
|
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"
|
|
|
|
local ref="" mounts=()
|
|
if [ "$mode" = local ]; then
|
|
git -C "$ROOT" ls-files --error-unmatch modules/sunrise/new.sh >/dev/null 2>&1 ||
|
|
fail "modules/sunrise/new.sh is not committed; the acceptance run clones HEAD"
|
|
git -C "$ROOT" diff --quiet HEAD -- modules/sunrise/new.sh ||
|
|
fail "modules/sunrise/new.sh differs from HEAD; commit it first (what is tested must be committed)"
|
|
ref="$(git -C "$ROOT" rev-parse HEAD)"
|
|
mounts=(-v "$ROOT:/fw:ro")
|
|
fi
|
|
|
|
echo "== docker pull node:24-bookworm"
|
|
docker pull -q node:24-bookworm >/dev/null
|
|
|
|
# Globals: the EXIT trap runs after this function returns.
|
|
acc_project="phase143acc-$$-$(date +%s)"
|
|
acc_runner="$acc_project-runner"
|
|
acc_cleanup() {
|
|
local rc=$?
|
|
docker rm -f "$acc_runner" >/dev/null 2>&1 || true
|
|
local ids
|
|
ids="$(docker ps -aq --filter "label=com.docker.compose.project=$acc_project")"
|
|
[ -z "$ids" ] || docker rm -f $ids >/dev/null 2>&1 || true
|
|
ids="$(docker volume ls -q --filter "label=com.docker.compose.project=$acc_project")"
|
|
[ -z "$ids" ] || docker volume rm -f $ids >/dev/null 2>&1 || true
|
|
ids="$(docker network ls -q --filter "label=com.docker.compose.project=$acc_project")"
|
|
[ -z "$ids" ] || docker network rm $ids >/dev/null 2>&1 || true
|
|
exit "$rc"
|
|
}
|
|
trap acc_cleanup EXIT
|
|
|
|
echo "== node:24-bookworm ($mode, compose project $acc_project)"
|
|
docker run --rm --name "$acc_runner" --network host \
|
|
-v /var/run/docker.sock:/var/run/docker.sock "${mounts[@]}" \
|
|
-e COMPOSE_PROJECT_NAME="$acc_project" -e ACC_MODE="$mode" -e ACC_REF="$ref" \
|
|
node:24-bookworm bash -euo pipefail -c "$acceptance_inner"
|
|
|
|
if [ "$mode" = production ]; then
|
|
echo "Phase 14.3 production acceptance passed"
|
|
else
|
|
echo "Phase 14.3 acceptance passed"
|
|
fi
|
|
}
|
|
|
|
stage_forbidden() {
|
|
cd "$ROOT"
|
|
echo "== forbidden surfaces"
|
|
local names
|
|
names="$(grep -noE 'SUMMER_[A-Z0-9_]*' modules/sunrise/new.sh | grep -v ':SUMMER_ADMIN__JWT__SECRET$' || true)"
|
|
if [ -n "$names" ]; then
|
|
printf '%s\n' "$names"
|
|
fail "new.sh names a framework config variable other than SUMMER_ADMIN__JWT__SECRET"
|
|
fi
|
|
if grep -nE '\$\{?SUMMER_|export[[:space:]]+SUMMER_' modules/sunrise/new.sh; then
|
|
fail "new.sh reads or exports a framework config variable"
|
|
fi
|
|
git rev-parse -q --verify 'refs/tags/v0.1.12^{commit}' >/dev/null || fail "tag v0.1.12 is missing; cannot compare go.mod"
|
|
local added
|
|
added="$(git diff v0.1.12 -- go.mod | grep -E '^\+[[:space:]]*(require[[:space:]]|[^+[:space:]]+[[:space:]]+v[0-9])' || true)"
|
|
[ -z "$added" ] || fail "go.mod gained a requirement since v0.1.12: $added"
|
|
local deps
|
|
deps="$(go list -deps ./...)"
|
|
if grep -xE 'plugin|.*yaegi.*' <<<"$deps"; then
|
|
fail "the dependency graph contains runtime plugin loading (plugin or yaegi)"
|
|
fi
|
|
local golden
|
|
for golden in internal/build/testdata/new/go.mod.golden internal/build/testdata/new/go.work.golden \
|
|
internal/build/testdata/new-no-plugin/go.mod.golden internal/build/testdata/new-no-plugin/go.work.golden; do
|
|
[ -f "$golden" ] || fail "$golden is missing"
|
|
if grep -nE '^[[:space:]]*/|=> /' "$golden"; then
|
|
fail "$golden contains an absolute path"
|
|
fi
|
|
done
|
|
local ignored
|
|
ignored="$(git ls-files internal/build/testdata | git check-ignore --no-index --stdin || true)"
|
|
[ -z "$ignored" ] || fail "the framework .gitignore ignores goldens: $ignored"
|
|
[ "$(git ls-files internal/build/testdata | wc -l)" -ge 23 ] || fail "fewer than 23 goldens are tracked"
|
|
echo "Phase 14.3 forbidden-surface scan passed"
|
|
}
|
|
|
|
stage_unit() {
|
|
cd "$ROOT"
|
|
for tool in go git pnpm python3 shellcheck; do
|
|
command -v "$tool" >/dev/null || fail "$tool is not installed"
|
|
done
|
|
unit_tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$unit_tmp"' EXIT
|
|
|
|
echo "== go vet ./..."
|
|
go vet ./...
|
|
|
|
echo "== go test ./... -count=1 -v"
|
|
if ! go test ./... -count=1 -v >"$unit_tmp/framework.log" 2>&1; then
|
|
grep -E '^[[:space:]]*--- FAIL|^FAIL|panic:' "$unit_tmp/framework.log" >&2 || tail -50 "$unit_tmp/framework.log" >&2
|
|
fail "go test ./... failed"
|
|
fi
|
|
require_named_pass "$unit_tmp/framework.log" "${REQUIRED_TESTS[@]}"
|
|
# Goldens change only with an explicit -update, never during the gate.
|
|
git diff --quiet -- internal/build/testdata || fail "golden files differ from the index"
|
|
[ -z "$(git status --porcelain -- internal/build/testdata)" ] || fail "untracked or changed golden files"
|
|
|
|
echo "== examples/hello"
|
|
go -C examples/hello test ./... -count=1
|
|
|
|
echo "== docs:build --check"
|
|
go run ./cmd/summer docs:build --check
|
|
|
|
echo "== site plugin"
|
|
[ -d "$SITE" ] || fail "site checkout $SITE is missing (set PHASE143_SITE)"
|
|
if ! go -C "$SITE/plugins/golem15/summercms" test ./... -count=1 -v >"$unit_tmp/site-plugin.log" 2>&1; then
|
|
tail -50 "$unit_tmp/site-plugin.log" >&2
|
|
fail "site plugin tests failed"
|
|
fi
|
|
require_named_pass "$unit_tmp/site-plugin.log" "${SITE_PLUGIN_TESTS[@]}"
|
|
|
|
echo "== site app"
|
|
if ! go -C "$SITE" test ./... -count=1 -v >"$unit_tmp/site-app.log" 2>&1; then
|
|
tail -50 "$unit_tmp/site-app.log" >&2
|
|
fail "site app tests failed"
|
|
fi
|
|
require_named_pass "$unit_tmp/site-app.log" "${SITE_APP_TESTS[@]}"
|
|
|
|
echo "== landing page: pnpm generate, pnpm test"
|
|
pnpm -C "$SITE/vue-summercmsio-app" generate >"$unit_tmp/generate.log" 2>&1 || {
|
|
tail -50 "$unit_tmp/generate.log" >&2
|
|
fail "pnpm generate failed"
|
|
}
|
|
pnpm -C "$SITE/vue-summercmsio-app" test
|
|
|
|
stage_forbidden
|
|
echo "Phase 14.3 unit stage passed"
|
|
}
|
|
|
|
stage_bootstrap() {
|
|
cd "$ROOT"
|
|
bootstrap_tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$bootstrap_tmp"' EXIT
|
|
echo "== SUMMERCMS_BOOTSTRAP_CHECK=1 TestBootstrapEndToEnd"
|
|
if ! SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/sunrise/ -run '^TestBootstrapEndToEnd$' -count=1 -v >"$bootstrap_tmp/log" 2>&1; then
|
|
tail -60 "$bootstrap_tmp/log" >&2
|
|
fail "TestBootstrapEndToEnd failed"
|
|
fi
|
|
require_named_pass "$bootstrap_tmp/log" TestBootstrapEndToEnd
|
|
echo "Phase 14.3 bootstrap check passed"
|
|
}
|
|
|
|
stage_terminal() {
|
|
cd "$ROOT"
|
|
command -v docker >/dev/null || fail "docker is not installed"
|
|
port_free 54320 || fail "127.0.0.1:54320 is in use"
|
|
port_free 8080 || fail "127.0.0.1:8080 is in use"
|
|
git diff --quiet HEAD -- modules/sunrise/new.sh ||
|
|
fail "modules/sunrise/new.sh differs from HEAD; commit it first (the check clones HEAD)"
|
|
terminal_tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$terminal_tmp"' EXIT
|
|
local head
|
|
head="$(git rev-parse HEAD)"
|
|
echo "== site TestTerminalCommands (local script, framework at $head)"
|
|
if ! SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_NEW_URL="file://$ROOT/modules/sunrise/new.sh" \
|
|
SUMMERCMS_REPO="$ROOT" SUMMERCMS_REF="$head" \
|
|
go -C "$SITE" test -run '^TestTerminalCommands$' -count=1 -v . >"$terminal_tmp/log" 2>&1; then
|
|
tail -60 "$terminal_tmp/log" >&2
|
|
fail "TestTerminalCommands failed"
|
|
fi
|
|
require_named_pass "$terminal_tmp/log" TestTerminalCommands
|
|
echo "Phase 14.3 terminal check passed"
|
|
}
|
|
|
|
stage_security() {
|
|
[ -f "$REVIEW" ] || fail "missing $REVIEW"
|
|
python3 - "$REVIEW" <<'PY' || fail "security review check failed"
|
|
import re, sys
|
|
text = open(sys.argv[1], encoding="utf-8").read()
|
|
want = [f"T-14.3-{i:02d}" for i in range(1, 27)] + ["T-14.3-SC"]
|
|
rows = {}
|
|
for line in text.splitlines():
|
|
if not line.startswith("| T-14.3-"):
|
|
continue
|
|
cells = [c.strip() for c in re.split(r"(?<!\\)\|", line)[1:-1]]
|
|
rows[cells[0]] = cells
|
|
bad = []
|
|
for tid in want:
|
|
if tid not in rows:
|
|
bad.append(f"{tid}: missing")
|
|
continue
|
|
cells = rows[tid]
|
|
if len(cells) < 9:
|
|
bad.append(f"{tid}: expected 9 columns, found {len(cells)}")
|
|
continue
|
|
severity, status, pinned = cells[3].lower(), cells[7].lower(), cells[8]
|
|
if status == "open":
|
|
bad.append(f"{tid}: status open")
|
|
if severity == "high":
|
|
if status != "mitigated":
|
|
bad.append(f"{tid}: high threat with status {status!r}")
|
|
if not re.search(r"Test[A-Z]\w+|--(unit|generator|bootstrap|terminal|acceptance|security|forbidden)\b|gate", pinned):
|
|
bad.append(f"{tid}: high threat pinned by no named test or gate stage ({pinned!r})")
|
|
for b in bad:
|
|
print("refuse: " + b, file=sys.stderr)
|
|
sys.exit(1 if bad else 0)
|
|
PY
|
|
echo "Phase 14.3 security check passed"
|
|
}
|
|
|
|
stage_all() {
|
|
local stage
|
|
for stage in unit generator bootstrap terminal acceptance security; do
|
|
echo "=== stage --$stage"
|
|
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
|
echo "PASS $stage"
|
|
else
|
|
echo "FAIL $stage" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Phase 14.3 gate passed"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
--all) stage_all ;;
|
|
--unit) stage_unit ;;
|
|
--forbidden) stage_forbidden ;;
|
|
--generator) stage_generator ;;
|
|
--bootstrap) stage_bootstrap ;;
|
|
--terminal) stage_terminal ;;
|
|
--acceptance) stage_acceptance local ;;
|
|
--acceptance-production) stage_acceptance production ;;
|
|
--security) stage_security ;;
|
|
*)
|
|
echo "usage: $0 --all | --unit | --forbidden | --generator | --bootstrap | --terminal | --acceptance | --acceptance-production | --security" >&2
|
|
exit 2
|
|
;;
|
|
esac
|