mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 15:47:50 +09:00
Issue #101 shipped because release.yml went straight from `build` to `push` with nothing in between. The image was missing scripts/, so every containerised panel 404'd its own enrollment URL, while the entire test suite stayed green -- pytest runs against the source tree, where the file always exists. Only booting the artifact catches that class of bug. scripts/test/smoke-docker-image.sh builds (or takes) an image, asserts the files the panel reads at request time are present, boots it, and requests every route that serves a file off disk. release.yml now builds amd64, runs it, and only then pushes multi-arch (layers are cached, so the push reuses the build). release-smoke.yml gets the same job on dev and PRs, and its path filters now include Dockerfile, .dockerignore and scripts/ -- none of which used to trigger it. scripts/test/verify-agent-release-chain.sh covers the other half, which unit tests structurally cannot: they stub curl, so they only ever prove the installer is self-consistent, never that the URL it builds resolves to a real file. This one reads the coordinates back out of the script, asks GitHub, downloads the archive, verifies the checksum and confirms the tarball entry name the installer moves. It runs nightly, and is also the tripwire for the reverse case -- the agent repo changing its release naming and breaking the panel from the other side of the repo split. Both are plain scripts so the same check runs locally: bash scripts/test/smoke-docker-image.sh bash scripts/test/verify-agent-release-chain.sh Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
146 lines
5.5 KiB
Bash
146 lines
5.5 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Smoke-test a BUILT ServerKit image.
|
|
#
|
|
# This is the check that issue #101 needed and nobody had. Everything the panel
|
|
# reads off its own filesystem at request time -- installers, the SPA bundle,
|
|
# the VERSION file -- exists in the source tree, so the whole pytest suite passes
|
|
# while the shipped image is missing the file. The only way to catch that class
|
|
# of bug is to boot the artifact you are about to publish and ask it for the
|
|
# things it promises to serve.
|
|
#
|
|
# Usage:
|
|
# bash scripts/test/smoke-docker-image.sh # builds serverkit:smoke, then tests
|
|
# bash scripts/test/smoke-docker-image.sh <image[:tag]> # tests an image you already have
|
|
# SKIP_BUILD=1 bash scripts/test/smoke-docker-image.sh # reuse the existing serverkit:smoke
|
|
#
|
|
# Exits non-zero on the first failed assertion, with the response that failed.
|
|
|
|
set -uo pipefail
|
|
|
|
IMAGE="${1:-serverkit:smoke}"
|
|
PORT="${SMOKE_PORT:-45990}"
|
|
CONTAINER="serverkit-smoke-$$"
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
ok() { PASS=$((PASS + 1)); printf ' \033[32m✔\033[0m %s\n' "$1"; }
|
|
bad() { FAIL=$((FAIL + 1)); printf ' \033[31m✘\033[0m %s\n' "$1"; }
|
|
|
|
cleanup() {
|
|
if [ -n "${CONTAINER:-}" ] && docker inspect "$CONTAINER" >/dev/null 2>&1; then
|
|
printf '\n--- container logs (tail) ---\n'
|
|
docker logs --tail 40 "$CONTAINER" 2>&1 || true
|
|
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Build (unless we were handed an image or told to skip)
|
|
# --------------------------------------------------------------------------
|
|
if [ -z "${SKIP_BUILD:-}" ] && [ "$IMAGE" = "serverkit:smoke" ]; then
|
|
printf '\nBuilding %s (this is the slow part)...\n' "$IMAGE"
|
|
if ! docker build -t "$IMAGE" "$REPO_ROOT"; then
|
|
printf '\n\033[31mimage build failed\033[0m\n'
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
printf '\nServerKit image smoke test — %s\n\n' "$IMAGE"
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Filesystem assertions: cheap, and they name the missing path directly rather
|
|
# than making you infer it from an HTTP status.
|
|
# --------------------------------------------------------------------------
|
|
for f in /app/scripts/install.sh /app/scripts/install.ps1 /app/VERSION \
|
|
/app/frontend/dist/index.html; do
|
|
if docker run --rm --entrypoint sh "$IMAGE" -c "test -f $f" 2>/dev/null; then
|
|
ok "image contains $f"
|
|
else
|
|
bad "image is MISSING $f — it will be requested at runtime"
|
|
fi
|
|
done
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Boot it
|
|
# --------------------------------------------------------------------------
|
|
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
if ! docker run -d --name "$CONTAINER" -p "${PORT}:5000" \
|
|
-e SECRET_KEY=smoke-secret \
|
|
-e JWT_SECRET_KEY=smoke-jwt \
|
|
"$IMAGE" >/dev/null; then
|
|
bad "container failed to start"
|
|
exit 1
|
|
fi
|
|
|
|
BASE="http://127.0.0.1:${PORT}"
|
|
printf ' waiting for %s/api/v1/system/health ...\n' "$BASE"
|
|
ready=""
|
|
for _ in $(seq 1 90); do
|
|
if curl -fsS -o /dev/null "${BASE}/api/v1/system/health" 2>/dev/null; then
|
|
ready=1
|
|
break
|
|
fi
|
|
# A container that has already exited will never become ready.
|
|
if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" != "true" ]; then
|
|
bad "container exited during boot"
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [ -n "$ready" ]; then
|
|
ok "panel boots and answers /api/v1/system/health"
|
|
else
|
|
bad "panel never became healthy"
|
|
exit 1
|
|
fi
|
|
|
|
# --------------------------------------------------------------------------
|
|
# HTTP assertions: every route that serves a file off the panel's own disk.
|
|
# --------------------------------------------------------------------------
|
|
assert_http() {
|
|
local path="$1" want="$2" desc="$3"
|
|
local code
|
|
code="$(curl -s -o /tmp/smoke-body.$$ -w '%{http_code}' "${BASE}${path}")"
|
|
if [ "$code" = "$want" ]; then
|
|
ok "$desc"
|
|
else
|
|
bad "$desc — got HTTP $code, want $want: $(head -c 200 /tmp/smoke-body.$$)"
|
|
fi
|
|
rm -f /tmp/smoke-body.$$
|
|
}
|
|
|
|
assert_body() {
|
|
local path="$1" needle="$2" desc="$3"
|
|
if curl -fsS "${BASE}${path}" 2>/dev/null | grep -q -- "$needle"; then
|
|
ok "$desc"
|
|
else
|
|
bad "$desc — '$needle' not found in the response body"
|
|
fi
|
|
}
|
|
|
|
# The exact request from issue #101.
|
|
assert_http /api/v1/servers/install.sh 200 "GET /api/v1/servers/install.sh serves the agent installer"
|
|
assert_http /api/v1/servers/install.ps1 200 "GET /api/v1/servers/install.ps1 serves the Windows installer"
|
|
assert_http / 200 "GET / serves the SPA"
|
|
|
|
assert_body /api/v1/servers/install.sh '#!/bin/bash' \
|
|
"the served install.sh is a real bash script"
|
|
# Placeholder substitution runs against the served copy; if it silently stopped
|
|
# working the box would try to enroll against a nonexistent domain.
|
|
if curl -fsS "${BASE}/api/v1/servers/install.sh" | grep -q 'your-serverkit.com'; then
|
|
bad "served install.sh still contains the your-serverkit.com placeholder"
|
|
else
|
|
ok "served install.sh has this panel's URL substituted in"
|
|
fi
|
|
assert_body /api/v1/servers/install.sh 'jhd3197/serverkit-agent' \
|
|
"the served install.sh downloads from the agent release repo"
|
|
assert_body /api/v1/servers/install.ps1 'Install-ServerKitAgent' \
|
|
"the served install.ps1 is a real PowerShell installer"
|
|
|
|
printf '\n%d passed, %d failed\n\n' "$PASS" "$FAIL"
|
|
[ "$FAIL" -eq 0 ]
|