feat: workspace tooling for the MesaStack fork

Add one-shot scripts driving both forks from the workspace root:

- build-image:   frontend build + thin-overlay image build + verification
- verify-image:  static and runtime smoke tests (brand, API key prefix,
                 login, real key creation), always cleans up its container
- sync-upstream: bump both forks to a new upstream release, cutting fresh
                 v<version>-lofyer branches and replaying fork-only commits
- bootstrap:     clone the two forks into place after cloning this repo

Fork-only commit detection deliberately goes beyond 'git log --cherry-pick
--right-only', which re-replays upstream commits that were rebased or
squashed into the new release; commits contained by any upstream ref or tag
are dropped instead.

The forks stay as separate repositories rather than submodules so each keeps
a clean upstream history to rebase onto new releases.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-27 09:16:05 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 004d18db92
7 changed files with 882 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# bootstrap: clone the two forks into this workspace.
#
# This repo intentionally tracks only the workspace-level tooling (AGENTS.md and
# scripts/); the frontend and backend forks stay in their own repositories so we
# keep clean upstream histories and can rebase onto new releases. Run this after
# cloning the workspace to lay out:
#
# gpustack-all-lofyer/
# ├── gpustack-ui-lofyer/ frontend fork
# └── gpustack-lofyer/ backend fork
#
# Usage:
# scripts/bootstrap # clone both, checkout the default version
# scripts/bootstrap v2.2.1 # checkout a specific v<version>-lofyer
#
# Existing clones are left alone (only their remotes are verified).
#
set -o errexit
set -o nounset
set -o pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
# Keep in sync with the version table in AGENTS.md.
DEFAULT_VERSION="v2.2.1"
VERSION="${1:-${DEFAULT_VERSION}}"
BRANCH="${VERSION}-lofyer"
require_cmd git
clone_fork() {
local name="${1}" dir="${2}" origin="${3}" upstream="${4}"
step "${name}"
if [[ -d "${dir}/.git" ]]; then
log "already present at ${dir}"
else
log "cloning ${origin}"
git clone "${origin}" "${dir}"
fi
if git -C "${dir}" remote get-url upstream >/dev/null 2>&1; then
git -C "${dir}" remote set-url upstream "${upstream}"
else
git -C "${dir}" remote add upstream "${upstream}"
fi
log "origin : $(git -C "${dir}" remote get-url origin)"
log "upstream: $(git -C "${dir}" remote get-url upstream)"
local current
current="$(repo_branch "${dir}")"
if [[ "${current}" == "${BRANCH}" ]]; then
log "already on ${BRANCH}"
elif git -C "${dir}" rev-parse -q --verify "refs/heads/${BRANCH}" >/dev/null; then
git -C "${dir}" checkout "${BRANCH}"
elif git -C "${dir}" rev-parse -q --verify "refs/remotes/origin/${BRANCH}" >/dev/null; then
git -C "${dir}" checkout -b "${BRANCH}" "origin/${BRANCH}"
else
warn "branch ${BRANCH} not found (staying on ${current}); fetch or pick another version"
fi
}
clone_fork "Frontend" "${UI_REPO}" "${UI_ORIGIN_URL}" "${UI_UPSTREAM_URL}"
clone_fork "Backend" "${BACKEND_REPO}" "${BACKEND_ORIGIN_URL}" "${BACKEND_UPSTREAM_URL}"
step "Done"
log "frontend: $(repo_branch "${UI_REPO}")"
log "backend : $(repo_branch "${BACKEND_REPO}")"
cat <<EOF
Next steps:
cd ${UI_REPO} && pnpm install # or npm install, per the frontend repo
scripts/build-image # build + verify the container image
EOF
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
#
# build-image: one-shot build of the MesaStack control-plane image from the
# workspace root. Builds the rebranded frontend, overlays it plus our patched
# backend sources onto the matching official release image, then (by default)
# smoke tests the result.
#
# It requires both forks to sit on the *same* v<version>-lofyer branch, so a
# build can never mix a frontend and backend from different upstream releases.
#
# Usage:
# scripts/build-image # build + verify
# scripts/build-image --no-verify # build only
# scripts/build-image --skip-ui-build # reuse existing frontend dist/
# scripts/build-image --push # push after a successful build
# scripts/build-image --dry-run # show what would happen
#
# Env knobs (also see gpustack-lofyer/hack/package-lofyer):
# NAMESPACE / REPOSITORY / TAG / IMAGE / BASE_IMAGE
# SMOKE_PORT host port used by the smoke test (default 18080)
#
set -o errexit
set -o nounset
set -o pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
UI_BUILD=1
VERIFY=1
PUSH=0
DRY_RUN=0
SMOKE_PORT="${SMOKE_PORT:-18080}"
while [[ $# -gt 0 ]]; do
case "${1}" in
--skip-ui-build) UI_BUILD=0 ;;
--no-verify) VERIFY=0 ;;
--verify) VERIFY=1 ;;
--push) PUSH=1 ;;
--dry-run) DRY_RUN=1 ;;
-h|--help) sed -n '2,26p' "${0}"; exit 0 ;;
*) fail "unknown argument: ${1}" ;;
esac
shift
done
require_cmd docker git
require_repos
VERSION="$(resolve_workspace_version)"
TAG="${TAG:-${VERSION}}"
IMAGE="${IMAGE:-${NAMESPACE}/${REPOSITORY}:${TAG}}"
log "workspace version : ${VERSION}"
log "target image : ${IMAGE}"
# 1. Frontend.
step "Building frontend"
if [[ "${UI_BUILD}" == "1" ]]; then
require_cmd npm
if [[ "${DRY_RUN}" == "1" ]]; then
echo "(dry-run) (cd ${UI_REPO} && npm run build)"
else
(cd "${UI_REPO}" && npm run build)
fi
else
log "skipping frontend build (--skip-ui-build)"
[[ -f "${UI_REPO}/dist/index.html" ]] \
|| fail "no existing frontend dist at ${UI_REPO}/dist (drop --skip-ui-build)"
fi
# 2. Image. Delegates to the backend packaging script, which stages the dist
# and only the python files that differ from the upstream tag.
step "Building image ${IMAGE}"
PACK_ENV=(
"NAMESPACE=${NAMESPACE}"
"REPOSITORY=${REPOSITORY}"
"TAG=${TAG}"
"IMAGE=${IMAGE}"
"UI_REPO=${UI_REPO}"
"PUSH=${PUSH}"
"DRY_RUN=${DRY_RUN}"
)
[[ -n "${BASE_IMAGE:-}" ]] && PACK_ENV+=("BASE_IMAGE=${BASE_IMAGE}")
env "${PACK_ENV[@]}" "${BACKEND_REPO}/hack/package-lofyer"
if [[ "${DRY_RUN}" == "1" ]]; then
log "dry-run complete"
exit 0
fi
# 3. Verify.
if [[ "${VERIFY}" == "1" ]]; then
step "Verifying ${IMAGE}"
IMAGE="${IMAGE}" SMOKE_PORT="${SMOKE_PORT}" "$(dirname "${BASH_SOURCE[0]}")/verify-image"
fi
step "Done"
log "image: ${IMAGE}"
log "run it with:"
echo " docker run -d --name mesastack -p 80:80 -v /var/run/docker.sock:/var/run/docker.sock ${IMAGE}"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# Shared helpers for the workspace-level scripts.
#
# Layout assumed by every script (see AGENTS.md):
#
# gpustack-all-lofyer/
# ├── scripts/
# ├── gpustack-ui-lofyer/ frontend fork
# └── gpustack-lofyer/ backend fork
#
# Resolve the workspace root from the calling script's location.
WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
UI_REPO="${UI_REPO:-${WORKSPACE_DIR}/gpustack-ui-lofyer}"
BACKEND_REPO="${BACKEND_REPO:-${WORKSPACE_DIR}/gpustack-lofyer}"
# Private registry (origin) and upstream GitHub remotes for both forks.
UI_ORIGIN_URL="${UI_ORIGIN_URL:-ssh://git@git.digiman.live:11022/root/gpustack-ui.git}"
UI_UPSTREAM_URL="${UI_UPSTREAM_URL:-https://github.com/gpustack/gpustack-ui.git}"
BACKEND_ORIGIN_URL="${BACKEND_ORIGIN_URL:-ssh://git@git.digiman.live:11022/root/gpustack.git}"
BACKEND_UPSTREAM_URL="${BACKEND_UPSTREAM_URL:-https://github.com/gpustack/gpustack.git}"
# Image coordinates.
NAMESPACE="${NAMESPACE:-mesastack}"
REPOSITORY="${REPOSITORY:-gpustack}"
_name="$(basename "${0}")"
log() { echo -e "\033[1;34m[${_name}]\033[0m $*"; }
warn() { echo -e "\033[1;33m[${_name}]\033[0m $*" >&2; }
fail() { echo -e "\033[1;31m[${_name}]\033[0m $*" >&2; exit 1; }
step() { echo -e "\n\033[1;36m==> $*\033[0m"; }
require_cmd() {
for c in "$@"; do
command -v "${c}" >/dev/null 2>&1 || fail "'${c}' is required but not installed"
done
}
require_repos() {
[[ -d "${UI_REPO}/.git" ]] || fail "frontend repo missing at ${UI_REPO} (run scripts/bootstrap)"
[[ -d "${BACKEND_REPO}/.git" ]] || fail "backend repo missing at ${BACKEND_REPO} (run scripts/bootstrap)"
}
# Current branch of a repo.
repo_branch() { git -C "${1}" rev-parse --abbrev-ref HEAD 2>/dev/null; }
# Extract the upstream version from a v<version>-lofyer branch name.
# Echoes the version (e.g. v2.2.1) or fails.
lofyer_branch_version() {
local branch="${1}"
if [[ "${branch}" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.]*)-lofyer$ ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
return 1
}
# Verify both forks sit on the same v<version>-lofyer branch and echo the version.
resolve_workspace_version() {
local ui_branch backend_branch ui_version backend_version
ui_branch="$(repo_branch "${UI_REPO}")"
backend_branch="$(repo_branch "${BACKEND_REPO}")"
ui_version="$(lofyer_branch_version "${ui_branch}")" \
|| fail "frontend is on '${ui_branch}', expected a v<version>-lofyer branch"
backend_version="$(lofyer_branch_version "${backend_branch}")" \
|| fail "backend is on '${backend_branch}', expected a v<version>-lofyer branch"
[[ "${ui_version}" == "${backend_version}" ]] \
|| fail "version mismatch: frontend=${ui_branch} backend=${backend_branch} (must match)"
echo "${ui_version}"
}
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
#
# sync-upstream: move both forks onto a new upstream release.
#
# For each repo it fetches upstream, cuts a fresh v<version>-lofyer branch from
# the target tag, and replays our fork-only commits onto it. Conflicts are left
# in the worktree for you to resolve (upstream frequently reflows docs or
# deletes files we keep), then re-run with --continue.
#
# Usage:
# scripts/sync-upstream --list # show available upstream versions
# scripts/sync-upstream v2.2.2 # bump both forks to v2.2.2
# scripts/sync-upstream v2.2.2 --dry-run # show the plan only
# scripts/sync-upstream --continue # resume after resolving conflicts
#
# Notes:
# - Refuses to run with dirty worktrees (except untracked files, which are
# yours and are left alone).
# - The tag must exist in BOTH upstreams, so frontend and backend never end up
# on different releases.
# - Nothing is pushed. Review, then push each repo yourself.
#
set -o errexit
set -o nounset
set -o pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
TARGET=""
LIST=0
DRY_RUN=0
CONTINUE=0
while [[ $# -gt 0 ]]; do
case "${1}" in
--list) LIST=1 ;;
--dry-run) DRY_RUN=1 ;;
--continue) CONTINUE=1 ;;
-h|--help) sed -n '2,24p' "${0}"; exit 0 ;;
v*) TARGET="${1}" ;;
*) fail "unknown argument: ${1}" ;;
esac
shift
done
require_cmd git
require_repos
# Ensure an 'upstream' remote exists and points at GitHub.
ensure_upstream_remote() {
local repo="${1}" url="${2}"
if git -C "${repo}" remote get-url upstream >/dev/null 2>&1; then
local current
current="$(git -C "${repo}" remote get-url upstream)"
if [[ "${current}" != "${url}" ]]; then
log " updating upstream remote: ${current} -> ${url}"
git -C "${repo}" remote set-url upstream "${url}"
fi
else
log " adding upstream remote -> ${url}"
git -C "${repo}" remote add upstream "${url}"
fi
}
fetch_upstream() {
local repo="${1}" url="${2}" name="${3}"
log "fetching upstream for ${name}"
ensure_upstream_remote "${repo}" "${url}"
git -C "${repo}" fetch --tags --prune upstream >/dev/null 2>&1
}
# Stable release tags only (drop rc/alpha/beta), newest last.
list_versions() {
git -C "${1}" tag --list 'v*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V
}
# --- resume path -------------------------------------------------------------
if [[ "${CONTINUE}" == "1" ]]; then
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
if [[ -d "${repo}/.git/sequencer" ]] || [[ -f "${repo}/.git/CHERRY_PICK_HEAD" ]]; then
step "Resuming cherry-pick in ${name}"
if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/ conflict: /'
fail "${name} still has unresolved conflicts; resolve and 'git add' them first"
fi
GIT_EDITOR=true git -C "${repo}" cherry-pick --continue || true
# The repo's lint hooks can stash/restore and upset the sequencer; if the
# commit landed, clear the leftover state so the next pick can proceed.
if [[ -d "${repo}/.git/sequencer" ]] && ! git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
git -C "${repo}" cherry-pick --quit 2>/dev/null || true
fi
log "${name}: resumed; re-run sync-upstream with the target version to replay remaining commits"
fi
done
exit 0
fi
# --- list path ---------------------------------------------------------------
fetch_upstream "${UI_REPO}" "${UI_UPSTREAM_URL}" "frontend"
fetch_upstream "${BACKEND_REPO}" "${BACKEND_UPSTREAM_URL}" "backend"
if [[ "${LIST}" == "1" ]]; then
step "Upstream stable releases present in BOTH repos"
comm -12 <(list_versions "${UI_REPO}") <(list_versions "${BACKEND_REPO}") | tail -15
step "Current"
log "frontend: $(repo_branch "${UI_REPO}")"
log "backend : $(repo_branch "${BACKEND_REPO}")"
exit 0
fi
[[ -n "${TARGET}" ]] || fail "no target version given (try --list)"
# --- preflight ---------------------------------------------------------------
step "Preflight"
CURRENT_VERSION="$(resolve_workspace_version)"
log "current version: ${CURRENT_VERSION}"
log "target version : ${TARGET}"
[[ "${TARGET}" != "${CURRENT_VERSION}" ]] || fail "already on ${TARGET}"
NEW_BRANCH="${TARGET}-lofyer"
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
git -C "${repo}" rev-parse -q --verify "refs/tags/${TARGET}" >/dev/null \
|| fail "${name}: tag ${TARGET} not found upstream"
# Tracked modifications block a clean branch cut; untracked files are the
# user's own work and are preserved across checkout.
if ! git -C "${repo}" diff-index --quiet HEAD -- 2>/dev/null; then
git -C "${repo}" status --short | grep -v '^??' | sed 's/^/ /'
fail "${name}: worktree has uncommitted changes; commit or stash them first"
fi
git -C "${repo}" rev-parse -q --verify "refs/heads/${NEW_BRANCH}" >/dev/null \
&& fail "${name}: branch ${NEW_BRANCH} already exists"
done
log "both repos are clean and ${TARGET} exists in both upstreams"
# Fork-only commits = on our branch but not in the new upstream tag, oldest first.
# Fork-only commits: on our branch, not reachable from the new tag, and not
# present on any upstream ref.
#
# `--cherry-pick --right-only` alone is not enough: upstream commits that were
# rebased or squashed into the new release get a different patch-id and would be
# replayed again. So we additionally drop anything that any upstream branch or
# tag contains, which is what actually distinguishes our work from upstream's.
fork_commits() {
local repo="${1}" old_branch="${2}" c
while read -r c; do
[[ -n "${c}" ]] || continue
# Skip commits reachable from any upstream remote branch or upstream tag.
if git -C "${repo}" branch -r --contains "${c}" 2>/dev/null | grep -q '^\s*upstream/'; then
continue
fi
if git -C "${repo}" tag --contains "${c}" 2>/dev/null | grep -q .; then
continue
fi
echo "${c}"
done < <(git -C "${repo}" log --reverse --no-merges --format='%H' \
"${TARGET}..${old_branch}" --cherry-pick --right-only)
}
# --- plan --------------------------------------------------------------------
step "Plan"
declare -A PICKS
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
old_branch="$(repo_branch "${repo}")"
mapfile -t commits < <(fork_commits "${repo}" "${old_branch}")
PICKS["${name}"]="${commits[*]:-}"
log "${name}: ${old_branch} -> ${NEW_BRANCH}, replaying ${#commits[@]} fork commit(s)"
for c in "${commits[@]:-}"; do
[[ -n "${c}" ]] && echo " $(git -C "${repo}" log --format='%h %s' -1 "${c}")"
done
done
if [[ "${DRY_RUN}" == "1" ]]; then
step "Dry run"
log "no changes made"
exit 0
fi
# --- execute -----------------------------------------------------------------
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
step "Updating ${name}"
git -C "${repo}" checkout -b "${NEW_BRANCH}" "${TARGET}"
log " cut ${NEW_BRANCH} from ${TARGET}"
read -r -a commits <<< "${PICKS[${name}]}"
for c in "${commits[@]:-}"; do
[[ -n "${c}" ]] || continue
subject="$(git -C "${repo}" log --format='%h %s' -1 "${c}")"
if git -C "${repo}" cherry-pick "${c}" >/dev/null 2>&1; then
log " picked ${subject}"
else
# A lint/pre-commit hook may have committed successfully while still
# returning non-zero; treat "no conflicts left" as success.
if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
warn " CONFLICT while picking ${subject}"
git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/ /'
cat >&2 <<EOF
Resolve the conflict in ${repo}, then:
git -C ${repo} add <files>
$(dirname "${BASH_SOURCE[0]}")/sync-upstream --continue
$(dirname "${BASH_SOURCE[0]}")/sync-upstream ${TARGET} # replay the rest
EOF
exit 1
fi
git -C "${repo}" cherry-pick --quit 2>/dev/null || true
log " picked ${subject} (hook noise ignored)"
fi
done
done
step "Done"
log "both forks are on ${NEW_BRANCH}"
cat <<EOF
Next steps:
1. Verify the build: scripts/build-image
2. Review the history: git -C ${UI_REPO} log --oneline ${TARGET}..HEAD
git -C ${BACKEND_REPO} log --oneline ${TARGET}..HEAD
3. Push both repos: git -C ${UI_REPO} push -u origin ${NEW_BRANCH}
git -C ${BACKEND_REPO} push -u origin ${NEW_BRANCH}
4. Update the version table in AGENTS.md
EOF
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
#
# verify-image: smoke test a built MesaStack image.
#
# Checks, in order:
# 1. static - gpustack version, API_KEY_PREFIX, UI assets carry our brand
# and no leftover upstream brand in index.html
# 2. runtime - boots the server, confirms / serves the MesaStack title,
# logs in and creates an API key to prove the mesastack_ prefix
# is live end to end, and that old gpustack_ keys are rejected
#
# Usage:
# scripts/verify-image # verify ${NAMESPACE}/${REPOSITORY}:<workspace version>
# IMAGE=mesastack/gpustack:v2.2.1 scripts/verify-image
# scripts/verify-image --static-only # skip booting a container
#
set -o errexit
set -o nounset
set -o pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
STATIC_ONLY=0
while [[ $# -gt 0 ]]; do
case "${1}" in
--static-only) STATIC_ONLY=1 ;;
-h|--help) sed -n '2,18p' "${0}"; exit 0 ;;
*) fail "unknown argument: ${1}" ;;
esac
shift
done
require_cmd docker curl python3
if [[ -z "${IMAGE:-}" ]]; then
require_repos
VERSION="$(resolve_workspace_version)"
IMAGE="${NAMESPACE}/${REPOSITORY}:${TAG:-${VERSION}}"
fi
SMOKE_PORT="${SMOKE_PORT:-18080}"
CONTAINER="${CONTAINER:-mesastack-verify}"
SITE_PACKAGES="/usr/local/lib/python3.11/dist-packages"
BRAND="${BRAND:-MesaStack}"
EXPECTED_PREFIX="${EXPECTED_PREFIX:-mesastack}"
docker image inspect "${IMAGE}" >/dev/null 2>&1 || fail "image not found locally: ${IMAGE}"
log "verifying ${IMAGE}"
failed=0
check() {
local label="${1}"; shift
if "$@" >/dev/null 2>&1; then
echo -e " \033[1;32mPASS\033[0m ${label}"
else
echo -e " \033[1;31mFAIL\033[0m ${label}"
failed=1
fi
}
# ---------- 1. Static checks ----------
step "Static checks"
docker run --rm --entrypoint bash "${IMAGE}" -c "
set -e
echo \"version: \$(gpustack version)\"
echo \"prefix : \$(python3 -c 'from gpustack.security import API_KEY_PREFIX; print(API_KEY_PREFIX)')\"
" || fail "image failed to report version/prefix"
check "API_KEY_PREFIX == ${EXPECTED_PREFIX}" \
docker run --rm --entrypoint python3 "${IMAGE}" -c \
"from gpustack.security import API_KEY_PREFIX as p; assert p == '${EXPECTED_PREFIX}', p"
check "old gpustack_ keys rejected" \
docker run --rm --entrypoint python3 "${IMAGE}" -c \
"from gpustack.security import is_valid_format as v; assert not v('gpustack_aaaa_bbbb')[0]"
check "masked value uses ${EXPECTED_PREFIX}_" \
docker run --rm --entrypoint python3 "${IMAGE}" -c \
"from gpustack.utils.api_keys import get_masked_api_key_value as m; assert m('abcd1234') == '${EXPECTED_PREFIX}_abcd***', m('abcd1234')"
check "UI index.html present" \
docker run --rm --entrypoint test "${IMAGE}" -f "${SITE_PACKAGES}/gpustack/ui/index.html"
check "UI carries ${BRAND} brand" \
docker run --rm --entrypoint grep "${IMAGE}" -q "${BRAND}" "${SITE_PACKAGES}/gpustack/ui/index.html"
check "no upstream GPUStack brand in UI index.html" \
docker run --rm --entrypoint bash "${IMAGE}" -c \
"! grep -q 'GPUStack' ${SITE_PACKAGES}/gpustack/ui/index.html"
if [[ "${STATIC_ONLY}" == "1" ]]; then
step "Result"
[[ "${failed}" == "0" ]] && { log "static checks passed"; exit 0; } || fail "static checks failed"
fi
# ---------- 2. Runtime checks ----------
step "Runtime checks (booting ${CONTAINER} on :${SMOKE_PORT})"
cleanup() {
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
rm -f "${COOKIES:-/dev/null}" 2>/dev/null || true
}
trap cleanup EXIT
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
docker run -d --name "${CONTAINER}" -p "${SMOKE_PORT}:80" "${IMAGE}" --disable-worker >/dev/null \
|| fail "failed to start container"
BASE="http://127.0.0.1:${SMOKE_PORT}"
log "waiting for server to become ready"
ready=0
for _ in $(seq 1 60); do
if curl -sS -m 5 -o /dev/null "${BASE}/" 2>/dev/null; then ready=1; break; fi
if ! docker ps --filter "name=${CONTAINER}" --format '{{.ID}}' | grep -q .; then
docker logs "${CONTAINER}" 2>&1 | tail -20
fail "container exited during startup"
fi
sleep 5
done
[[ "${ready}" == "1" ]] || { docker logs "${CONTAINER}" 2>&1 | tail -20; fail "server did not become ready"; }
check "/ serves ${BRAND} title" \
bash -c "curl -sS -m 15 '${BASE}/' | grep -q '<title>${BRAND}</title>'"
COOKIES="$(mktemp)"
PASSWORD="$(docker exec "${CONTAINER}" cat /var/lib/gpustack/initial_admin_password 2>/dev/null || echo "")"
if [[ -z "${PASSWORD}" ]]; then
warn "bootstrap password not found; skipping API key check"
else
login_status="$(curl -sS -c "${COOKIES}" -m 20 -o /dev/null -w '%{http_code}' \
-X POST "${BASE}/auth/login" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "username=admin" \
--data-urlencode "password=${PASSWORD}" || echo "000")"
check "admin login succeeds" test "${login_status}" = "200"
if [[ "${login_status}" == "200" ]]; then
key_value="$(curl -sS -b "${COOKIES}" -m 20 -X POST "${BASE}/v2/api-keys" \
-H 'Content-Type: application/json' \
-d '{"name":"verify-image","description":"prefix check"}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("value",""))' 2>/dev/null || echo "")"
log "generated key: ${key_value%%_*}_<redacted>"
check "generated key uses ${EXPECTED_PREFIX}_ prefix" \
bash -c "[[ '${key_value}' == ${EXPECTED_PREFIX}_* ]]"
fi
fi
step "Result"
if [[ "${failed}" == "0" ]]; then
log "all checks passed for ${IMAGE}"
else
fail "one or more checks failed for ${IMAGE}"
fi