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
+8
View File
@@ -0,0 +1,8 @@
# The two forks are separate repositories cloned in by scripts/bootstrap,
# not submodules, so they are never tracked here.
/gpustack-ui-lofyer/
/gpustack-lofyer/
# Local build/verify scratch
*.log
.DS_Store
+234
View File
@@ -0,0 +1,234 @@
# Agent Instructions - MesaStack (GPUStack downstream fork)
This is the **combined workspace** for our MesaStack product, a downstream fork of
[GPUStack](https://github.com/gpustack). It bundles both the frontend and backend
repos so agents and contributors work from a single root going forward.
## Layout
```
gpustack-all-lofyer/ <- this repo (tooling only)
├── AGENTS.md # workspace-level conventions
├── scripts/
│ ├── bootstrap # clone the two forks into place
│ ├── build-image # frontend build + image build + verify
│ ├── verify-image # smoke test a built image
│ ├── sync-upstream # bump both forks to a new upstream release
│ └── lib/common.sh # shared helpers
├── gpustack-ui-lofyer/ # frontend fork (upstream: gpustack/gpustack-ui)
└── gpustack-lofyer/ # backend fork (upstream: gpustack/gpustack)
```
This repo tracks **only the workspace tooling**. The two forks live in their own
repositories and are *not* submodules, so each keeps a clean upstream history that
can be rebased onto new releases. After cloning this repo, run `scripts/bootstrap`
to lay the forks out at the paths above.
Each sub-repo is an independent git repository with its own history, remotes, and
`AGENTS.md` / `CLAUDE.md`. Read the sub-repo's own instructions before working in it.
Remotes (`origin` = private registry, `upstream` = GitHub):
| Repo | origin |
| ------------------ | --------------------------------------------------- |
| gpustack-all-lofyer| `ssh://git@git.digiman.live:11022/root/gpustack-all-lofyer.git` |
| gpustack-ui-lofyer | `ssh://git@git.digiman.live:11022/root/gpustack-ui.git` |
| gpustack-lofyer | `ssh://git@git.digiman.live:11022/root/gpustack.git` |
## Workspace scripts
Run these from the workspace root. Every script refuses to proceed unless both
forks sit on the **same** `v<version>-lofyer` branch, so a build can never mix a
frontend and backend from different upstream releases.
```bash
scripts/bootstrap [v<version>] # clone both forks, checkout v<version>-lofyer
scripts/build-image # npm build + image build + full verification
scripts/build-image --skip-ui-build # reuse an existing frontend dist/
scripts/build-image --no-verify # build only
scripts/build-image --push # push the image after a green build
scripts/build-image --dry-run # show the plan
scripts/verify-image # smoke test the current version's image
scripts/verify-image --static-only # skip booting a container
scripts/sync-upstream --list # upstream releases available in BOTH repos
scripts/sync-upstream v2.2.2 # bump both forks to v2.2.2
scripts/sync-upstream v2.2.2 --dry-run
scripts/sync-upstream --continue # resume after resolving a cherry-pick conflict
```
## Versioning
Both repos track the **same upstream release** and keep customizations on a
`v<upstream-version>-lofyer` branch. Frontend and backend are always bumped
**together** to the same upstream tag. Current target: **v2.2.1**.
| Repo | Branch | Upstream base | Upstream repo |
| ------------------ | --------------- | ------------- | ----------------------- |
| gpustack-ui-lofyer | `v2.2.1-lofyer` | tag `v2.2.1` | `gpustack/gpustack-ui` |
| gpustack-lofyer | `v2.2.1-lofyer` | tag `v2.2.1` | `gpustack/gpustack` |
## Branch rules
- **Never commit customizations to `main` or to any upstream branch.** `main`
mirrors upstream and must stay clean so it can be fast-forwarded/mirrored.
- All customization work lives on `v<upstream-version>-lofyer` (e.g.
`v2.2.1-lofyer`), one such branch per upstream release we follow.
- One upstream version = one `-lofyer` branch. Do **not** reuse an old branch for a
new upstream version; cut a fresh one from the new tag and re-apply commits.
- Keep the frontend and backend `-lofyer` branch names in lockstep (same version
suffix) so a customer build always pairs matching branches.
- Only fast-forward-safe, curated customization commits belong on `-lofyer`. Prefer
small, cherry-pickable commits so the next version bump replays cleanly.
## Updating to a new upstream version
Use `scripts/sync-upstream`, which does the whole bump for both repos:
```bash
scripts/sync-upstream --list # releases available in BOTH upstreams
scripts/sync-upstream v2.2.2 --dry-run # review the replay plan first
scripts/sync-upstream v2.2.2 # cut branches + replay fork commits
scripts/build-image # verify the result end to end
git -C gpustack-ui-lofyer push -u origin v2.2.2-lofyer
git -C gpustack-lofyer push -u origin v2.2.2-lofyer
# then update the version table + "Current target" above
```
What it does per repo: fetch `upstream`, require a clean worktree and that the tag
exists in **both** upstreams, cut `v<version>-lofyer` from the tag, then cherry-pick
our fork-only commits oldest-first. On conflict it stops and tells you exactly what
to resolve; afterwards run `scripts/sync-upstream --continue`, then re-run with the
target version to replay the remainder. Nothing is pushed automatically.
**Identifying fork-only commits** is the subtle part, and the script handles it:
`git log --cherry-pick --right-only` alone is *not* sufficient, because upstream
commits that were rebased or squashed into the new release get a different patch-id
and would be replayed a second time. The script therefore also drops any commit that
an `upstream/*` branch or any tag contains. If you ever do this by hand, verify with
`git branch -r --contains <sha> | grep upstream`.
Untracked files (e.g. work in progress under `src/pages/`) are treated as yours and
left untouched; only tracked modifications block the bump.
## Customizations to re-apply each version
### Frontend (`gpustack-ui-lofyer`)
Fork-only commits carried on top of upstream. Details in
`gpustack-ui-lofyer/AGENTS.md` + `CLAUDE.md`. Notably:
- `scripts/sync-github` and `scripts/rebrand` (the tooling itself).
- Rebrand user-facing `GPUStack` -> `MesaStack` (run `scripts/rebrand`).
- Footer/topbar trim, navigation moved to top header.
### Backend (`gpustack-lofyer`)
- **API key / token prefix**: `API_KEY_PREFIX` in `gpustack/security.py` is set to
`mesastack` (upstream `gpustack`). This drives API keys, worker registration
tokens, cluster registration tokens (`mesastack_{access}_{secret}`), and the
masked display value. We intentionally **do not** stay backward compatible with
old `gpustack_` keys (fresh customer deployments only). Keep the assertions in
`tests/utils/test_api_keys.py` in sync with the prefix.
- **Image build tooling**: `pack/Dockerfile.lofyer` + `hack/package-lofyer` (thin
overlay build, see "Shipping a customer build" below). These are fork-only files,
so cherry-pick them onto each new `-lofyer` branch.
Note the backend `origin` points at the private registry and `upstream` at GitHub,
mirroring the frontend convention:
```
upstream https://github.com/gpustack/gpustack
origin ssh://git@192.168.0.23:11022/root/gpustack.git
```
## Brand rules
User-facing brand is **MesaStack** (upstream **GPUStack**). Functional identifiers
are **not** rebranded, to preserve compatibility with upstream contracts across the
frontend/backend boundary:
- lowercase `gpustack` (npm/pip package, URLs, paths, k8s namespace)
- `GPUSTACK_*` env constants
- `X-*` HTTP headers, cookies (`gpustack_session`, `gpustack_oidc_*`)
- JS/Python identifiers (e.g. `getGPUStackPlugin`, `gpustack_worker` proctitles)
- backend-contract strings (see `SKIP_LINE_PATTERNS` in frontend `scripts/rebrand`)
The API key **prefix** (`mesastack`) is a deliberate exception: it is user-facing
and, by our choice, not upstream-compatible.
## Shipping a customer build
A release requires **both** the rebranded frontend and the backend on matching
`v<version>-lofyer` branches. The deliverable is a single control-plane container
image built by `gpustack-lofyer/hack/package-lofyer`.
### Building the image
One command from the workspace root:
```bash
scripts/build-image # frontend build -> image -> full verification
```
It resolves the version from the (matching) `v<version>-lofyer` branches, builds the
rebranded frontend, delegates to `gpustack-lofyer/hack/package-lofyer` for the image,
then runs `scripts/verify-image`. Flags: `--skip-ui-build`, `--no-verify`, `--push`,
`--dry-run`. Env: `NAMESPACE`, `REPOSITORY`, `TAG`, `IMAGE`, `BASE_IMAGE`,
`SMOKE_PORT`.
The backend script can also be used directly if you only want the image step:
```bash
cd gpustack-lofyer && ./hack/package-lofyer # -> mesastack/gpustack:v<version>
```
It derives the upstream tag from the current branch name, so it always overlays a
matching official base image. Knobs: `UI_BUILD=1`, `DRY_RUN=1`, `PUSH=1`, `TAG`,
`IMAGE`, `BASE_IMAGE`, `NAMESPACE`, `REPOSITORY`, `UI_REPO`.
### Why a thin overlay, not `make package`
Upstream `pack/Dockerfile` rebuilds the entire control plane (PostgreSQL 17,
Higress gateway stack, Prometheus/Grafana, Skopeo compiled from source, ROCm
`amd_smi`, vLLM extras). Those components are hardcoded and cannot be skipped, so a
full `make package` costs 30-60+ minutes and tens of GB. Our fork only diverges in
two places, so `pack/Dockerfile.lofyer` overlays them onto the official release
image (`gpustack/gpustack:v<version>`) in seconds:
1. `gpustack/ui/` replaced with our rebranded frontend `dist/`
2. fork-patched Python sources (currently just `gpustack/security.py`)
The script stages only files that actually differ from the upstream tag
(`git diff <tag>..HEAD -- 'gpustack/**/*.py'`), keeping the layer small and
auditable. The Dockerfile self-checks the result at build time (asserts
`API_KEY_PREFIX == mesastack`, and that the UI carries the MesaStack brand).
Note this image is the **control plane** (server + worker). Inference runners use
separate `gpustack-runner` images pulled at runtime and are unaffected.
If you ever need a fully self-built image with no upstream base (clean supply
chain), use `PACKAGE_UI_DOWNLOAD=false make package` after staging our `dist/`
into `gpustack/ui/`, and expect the long build.
### Verifying a build
`scripts/build-image` runs this automatically; use it standalone to re-check an
existing image:
```bash
scripts/verify-image # static + runtime checks
scripts/verify-image --static-only # skip booting a container
IMAGE=mesastack/gpustack:v2.2.1 scripts/verify-image
```
Checks performed:
- **static**: `gpustack version`, `API_KEY_PREFIX == mesastack`, old `gpustack_`
keys rejected, masked value format, UI assets present, UI carries the MesaStack
brand, no leftover `GPUStack` string in `index.html`
- **runtime**: boots with `--disable-worker`, confirms `/` serves
`<title>MesaStack</title>`, logs in via `POST /auth/login` (bootstrap password at
`/var/lib/gpustack/initial_admin_password`) and creates a key via
`POST /v2/api-keys` to prove the `mesastack_` prefix is live end to end
The test container is always removed, including on failure.
+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