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:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
004d18db92
Executable
+76
@@ -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
|
||||
Executable
+102
@@ -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}"
|
||||
@@ -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}"
|
||||
}
|
||||
Executable
+233
@@ -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
|
||||
Executable
+153
@@ -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
|
||||
Reference in New Issue
Block a user