feat(bootstrap): game stack 按 lock 文件固定构建并校验 sha256,基础镜像按 digest 固定,JRE 固定补丁版本

This commit is contained in:
Lemon-miaow committed 2026-09-25 00:24:02 +08:00
1 parent aace66e8d3
commit 82545548e7
13 files changed
+632 -94

No files matched your search

+223 -47
View File
@@ -46,6 +46,14 @@
# proxy instead of the stock download (default: unset, stock).
# FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar
# above is set; the install refuses on a mismatch.
# FELIS_GAME_STACK pinned|latest — which Limbo, Paper, LuckPerms and Velocity builds to
# install (default: pinned, the builds deploy/game-stack.lock names,
# each checked against its sha256). latest resolves upstream's newest
# builds on every run; the Minecraft version then follows Limbo's CI.
# FELIS_JRE_VERSION Temurin feature version for the proxy (default: 25, whose build and
# digests are pinned; a rerun moves an installer-managed JRE to the
# pinned build). Another feature version is checked against the
# digest Adoptium's API publishes for it.
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
# FELIS_GO_SHA256 sha256 of that version's linux tarball for this host's architecture.
# REQUIRED for a non-default FELIS_GO_VERSION; the default's is pinned.
@@ -239,12 +247,17 @@ FELIS_LOBBY_IMAGE="${FELIS_LOBBY_IMAGE:-${REGISTRY_URL}/felis/lobby:demo}"
# server — forwarding is applied by the operator's init-forwarding initContainer, so it
# needs no secret. Seeded recommended in 0019_recommended_paper.sql.
FELIS_PAPER_IMAGE="${FELIS_PAPER_IMAGE:-${REGISTRY_URL}/felis/paper:demo}"
# The Velocity MINOR is pinned, not discovered. PaperMC's Fill v3 groups velocity
# builds by version group, and "newest across all groups" today means 4.0.0-SNAPSHOT —
# an UNRELEASED proxy (the 4.0.0 group has zero published builds) that needs a Java 25
# runtime. Crossing a major is a deliberate code change, so we track the newest BUILD of
# a pinned minor and let a human move the pin.
FELIS_VELOCITY_VERSION="${FELIS_VELOCITY_VERSION:-3.5.1}"
# The Velocity build comes from deploy/game-stack.lock (VELOCITY_VERSION and its jar digest).
# Setting FELIS_VELOCITY_VERSION to another version, or FELIS_GAME_STACK=latest, installs
# the newest BUILD of that minor instead. The minor itself is never discovered: PaperMC's
# Fill v3 groups velocity builds by version group, and "newest across all groups" can mean
# an unreleased 4.x SNAPSHOT that needs another runtime. Crossing a major is a deliberate
# code change.
FELIS_VELOCITY_VERSION="${FELIS_VELOCITY_VERSION:-}"
VELOCITY_LATEST_MINOR="3.5.1"
# pinned installs the builds deploy/game-stack.lock names (resolve_game_jars); latest asks
# upstream for its newest ones, which is how that lock file gets refreshed.
FELIS_GAME_STACK="${FELIS_GAME_STACK:-pinned}"
# Path to a Felis-Legacy Velocity fork build, installed as the proxy in place of the
# stock download. Unset — the default — changes nothing.
#
@@ -275,6 +288,14 @@ FELIS_VELOCITY_FORK_JAR_SHA256="${FELIS_VELOCITY_FORK_JAR_SHA256:-}"
# a lottery across four package managers — a tarball is one code path everywhere (same
# reasoning as install_go_toolchain).
FELIS_JRE_VERSION="${FELIS_JRE_VERSION:-25}"
# The JRE the proxy runs on is unpacked as root and carries every player session, so the
# default feature version is pinned to one build and the sha256 Adoptium publishes for each
# architecture. Move the three together (the Adoptium API lists them:
# /v3/assets/latest/25/hotspot?image_type=jre&os=linux).
JRE_PINNED_FEATURE="25"
JRE_PINNED_RELEASE="25.0.4.1+1"
JRE_PINNED_SHA256_X64="1731a34baadec5479258ea0202e4d5d865d2efeee60cb0c7d7eb056fe96ca219"
JRE_PINNED_SHA256_AARCH64="34828cbb93ed31c281c84ecb31ddab655d11a802f263c1fc019d42e9e0230fed"
# The port the proxy listens on: the ONLY Minecraft port players ever touch. Backends
# are ClusterIP-only, verify the modern-forwarding HMAC, and use NetworkPolicy to limit
# non-node ingress to the declared proxy CIDRs.
@@ -671,6 +692,10 @@ validate_settings() {
validate_listen FELIS_NANO_LISTEN "$FELIS_NANO_LISTEN"
validate_cidr FELIS_NANO_PROXY_CIDR "$FELIS_NANO_PROXY_CIDR"
validate_offsite_settings
case "$FELIS_GAME_STACK" in
pinned|latest) ;;
*) die "FELIS_GAME_STACK must be pinned or latest (got '${FELIS_GAME_STACK}')" ;;
esac
}
# validate_offsite_settings checks the FELIS_OFFSITE_* inputs before anything is
@@ -1198,7 +1223,7 @@ github_api() {
# can never disagree about what "latest" means.
github_latest_tag() {
local json tag
# Fetch first, filter second — the SIGPIPE reason documented on resolve_game_jars.
# Fetch first, filter second — the SIGPIPE reason documented on resolve_latest_game_jars.
json="$(github_api "repos/$(repo_slug)/releases/latest")" || return 1
tag="$(printf '%s' "$json" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' || true)"
tag="${tag%%$'\n'*}"
@@ -1236,7 +1261,7 @@ felis_asset_arch() {
# reachable this way — fetch releases/assets/<id> with the JSON Accept if that is ever needed.
github_asset_id() {
local tag="$1" name="$2" json id
# Fetch first, filter second — the SIGPIPE reason documented on resolve_game_jars.
# Fetch first, filter second — the SIGPIPE reason documented on resolve_latest_game_jars.
json="$(github_api "repos/$(repo_slug)/releases/tags/${tag}")" || return 1
id="$(printf '%s' "$json" | tr -d '\n' | tr '{' '\n' \
| grep "\"name\":[[:space:]]*\"${name}\"" \
@@ -1685,11 +1710,6 @@ game_stack_source() {
ok "game-stack sources unpacked to ${GAME_STACK_DIR}"
}
# resolve_game_jars pins Limbo and Paper to the SAME Minecraft version. LOOHP/Limbo
# speaks exactly one protocol per build, so the login gate dictates the version and Paper
# follows — a client that can pass the gate must also be able to reach the lobby.
# MC_VERSION is read off Limbo's CI artifact name (Limbo-<limbo-ver>-<mc-ver>.jar), which
# is the only place the pairing is published.
# meta_get prints a small metadata document. curl's --retry covers transient HTTP
# statuses and timeouts; a TLS handshake cut mid-way (exit 35, seen against Fill over
# a flaky IPv6 path) is outside its retry set, so the outer loop retries every failure
@@ -1710,8 +1730,71 @@ meta_get() {
return 1
}
# resolve_game_jars sets the artifacts the three game images and the proxy are built from:
# the builds deploy/game-stack.lock names (FELIS_GAME_STACK=pinned, the default), or
# upstream's newest ones (latest). Pinned is what makes an install reproducible: every host
# installing one release gets the same login gate, lobby and proxy, each download is
# checked against the lock's sha256, and a rerun's docker builds hit their cache, so
# restart_existing_system_servers leaves the login and lobby pods running.
resolve_game_jars() {
local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta file base rest paper
if [ "$FELIS_GAME_STACK" = "latest" ]; then
resolve_latest_game_jars
return 0
fi
load_game_stack_lock "${GAME_STACK_DIR}/deploy/game-stack.lock"
ok "Limbo ${LIMBO_VERSION} + Paper on Minecraft ${MC_VERSION}, LuckPerms and Velocity ${VELOCITY_VERSION}: the builds game-stack.lock pins"
}
GAME_STACK_LOCK_KEYS="MC_VERSION LIMBO_VERSION LIMBO_JAR_URL LIMBO_JAR_SHA256 LIMBO_SCHEM_URL LIMBO_SCHEM_SHA256 PAPER_JAR_URL PAPER_JAR_SHA256 LUCKPERMS_JAR_URL LUCKPERMS_JAR_SHA256 VELOCITY_VERSION VELOCITY_JAR_URL VELOCITY_JAR_SHA256"
# load_game_stack_lock reads the lock's KEY=value lines into the globals of the same names.
# It never sources the file: only the keys above are accepted, every one has to be set, the
# values are limited to URL and version characters (they reach docker build-args), and each
# *_SHA256 has to be a sha256.
load_game_stack_lock() {
local file="$1" line key value
[ -f "$file" ] || die "no game-stack lock at ${file}; FELIS_GAME_STACK=latest resolves upstream's newest builds instead"
for key in $GAME_STACK_LOCK_KEYS; do printf -v "$key" '%s' ""; done
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in ''|'#'*) continue ;; esac
key="${line%%=*}"
value="${line#*=}"
[ "$key" != "$line" ] || die "${file}: not a KEY=value line: ${line}"
case " ${GAME_STACK_LOCK_KEYS} " in
*" ${key} "*) ;;
*) die "${file}: unknown key ${key}" ;;
esac
case "$value" in
''|*[!A-Za-z0-9._:/+%-]*) die "${file}: ${key} has an unexpected value: ${value}" ;;
esac
printf -v "$key" '%s' "$value"
done < "$file"
for key in $GAME_STACK_LOCK_KEYS; do
[ -n "${!key}" ] || die "${file} does not set ${key}"
case "$key" in
*_SHA256) [[ "${!key}" =~ ^[0-9a-f]{64}$ ]] || die "${file}: ${key} is not a lowercase sha256" ;;
esac
done
}
# url_sha256 prints the sha256 of what $1 serves.
url_sha256() {
local sum
sum="$(curl -fsSL --retry 5 --retry-delay 2 -A "felis-bootstrap (+https://github.com/FelisMC/Felis)" "$1" | sha256sum)" || return 1
printf '%s\n' "${sum%% *}"
}
# resolve_latest_game_jars pins Limbo and Paper to the SAME Minecraft version. LOOHP/Limbo
# speaks exactly one protocol per build, so the login gate dictates the version and Paper
# follows — a client that can pass the gate must also be able to reach the lobby.
# MC_VERSION is read off Limbo's CI artifact name (Limbo-<limbo-ver>-<mc-ver>.jar), which
# is the only place the pairing is published.
#
# Limbo's CI and LuckPerms publish no digest, so latest hashes their downloads as it finds
# them: the image builds still check that they receive those bytes, but nothing vouches for
# the bytes themselves. That is what the lock file adds.
resolve_latest_game_jars() {
local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta build file base rest paper
log "resolving the newest LOOHP/Limbo CI build"
# Fetch first, filter second: `curl | grep | head` dies of SIGPIPE under `set -o pipefail`
# the moment head closes the pipe early. Same shape everywhere below.
@@ -1720,6 +1803,13 @@ resolve_game_jars() {
file="$(printf '%s' "$meta" | grep -o 'Limbo-[0-9A-Za-z._-]*\.jar' || true)"
file="${file%%$'\n'*}"
[ -n "$file" ] || die "no Limbo jar in the LOOHP/Limbo CI artifact list"
# The numbered build, so the jar hashed below is the jar the image build downloads even
# if CI finishes another build in between.
build="$(printf '%s' "$meta" | grep -o '"number":[0-9]*' || true)"
build="${build%%$'\n'*}"
build="${build#*:}"
[ -n "$build" ] || die "no build number in the LOOHP/Limbo CI metadata"
ci="https://ci.loohpjames.com/job/Limbo/${build}"
base="${file%.jar}" # Limbo-2026.0.2-ALPHA-26.2
MC_VERSION="${base##*-}" # 26.2
@@ -1742,7 +1832,16 @@ resolve_game_jars() {
log "resolving the newest LuckPerms build"
LUCKPERMS_JAR_URL="$(luckperms_latest_jar)" \
|| die "could not resolve a LuckPerms build (metadata.luckperms.net is down or flapping); the lobby needs it for the panel's permission controls"
ok "Limbo ${LIMBO_VERSION} + Paper, both on Minecraft ${MC_VERSION}; LuckPerms resolved"
log "hashing the Limbo and LuckPerms downloads (their upstreams publish no digest)"
LIMBO_JAR_SHA256="$(url_sha256 "$LIMBO_JAR_URL")" || die "could not download ${LIMBO_JAR_URL}"
LIMBO_SCHEM_SHA256="$(url_sha256 "$LIMBO_SCHEM_URL")" || die "could not download ${LIMBO_SCHEM_URL}"
LUCKPERMS_JAR_SHA256="$(url_sha256 "$LUCKPERMS_JAR_URL")" || die "could not download ${LUCKPERMS_JAR_URL}"
VELOCITY_VERSION="$VELOCITY_LATEST_MINOR"
VELOCITY_JAR_URL=""
VELOCITY_JAR_SHA256=""
ok "Limbo ${LIMBO_VERSION} (CI build ${build}) + Paper, both on Minecraft ${MC_VERSION}; LuckPerms resolved"
warn "FELIS_GAME_STACK=latest: these are upstream's builds as of now, not the ones this release pins"
}
# luckperms_latest_jar prints the download URL of the current LuckPerms Bukkit build.
@@ -1792,7 +1891,9 @@ build_game_stack() {
log "building ${FELIS_LIMBO_IMAGE} (LOOHP/Limbo ${LIMBO_VERSION}, Minecraft ${MC_VERSION})"
docker build -f "${GAME_STACK_DIR}/deploy/limbo/Dockerfile" \
--build-arg LIMBO_JAR_URL="$LIMBO_JAR_URL" \
--build-arg LIMBO_JAR_SHA256="$LIMBO_JAR_SHA256" \
--build-arg LIMBO_SCHEM_URL="$LIMBO_SCHEM_URL" \
--build-arg LIMBO_SCHEM_SHA256="$LIMBO_SCHEM_SHA256" \
--build-arg LIMBO_VERSION="$LIMBO_VERSION" \
-t "$FELIS_LIMBO_IMAGE" "$GAME_STACK_DIR"
@@ -1801,6 +1902,7 @@ build_game_stack() {
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
--build-arg LUCKPERMS_JAR_URL="$LUCKPERMS_JAR_URL" \
--build-arg LUCKPERMS_JAR_SHA256="$LUCKPERMS_JAR_SHA256" \
-t "$FELIS_LOBBY_IMAGE" "$GAME_STACK_DIR"
# Plain Paper, same MC_VERSION and PAPER_JAR_URL (no new dependency). Forwarding is the
@@ -1985,29 +2087,84 @@ pin_via_block_connections() {
}
install_jre() {
local arch url
if [ -x "${JRE_DIR}/bin/java" ]; then
ok "JRE already installed at ${JRE_DIR}"
return 0
fi
local arch want url release json
case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
aarch64|arm64) arch="aarch64" ;;
*) die "no Temurin JRE build for architecture $(uname -m); pre-stage one at ${JRE_DIR}" ;;
x86_64|amd64) arch="x64"; want="$JRE_PINNED_SHA256_X64" ;;
aarch64|arm64) arch="aarch64"; want="$JRE_PINNED_SHA256_AARCH64" ;;
*)
if [ -x "${JRE_DIR}/bin/java" ]; then
ok "JRE already installed at ${JRE_DIR}"
return 0
fi
die "no Temurin JRE build for architecture $(uname -m); pre-stage one at ${JRE_DIR}"
;;
esac
url="https://api.adoptium.net/v3/binary/latest/${FELIS_JRE_VERSION}/ga/linux/${arch}/jre/hotspot/normal/eclipse"
log "installing Temurin ${FELIS_JRE_VERSION} JRE (${arch}) to ${JRE_DIR}"
local tmp
if [ "$FELIS_JRE_VERSION" = "$JRE_PINNED_FEATURE" ]; then
release="$JRE_PINNED_RELEASE"
url="https://github.com/adoptium/temurin${JRE_PINNED_FEATURE}-binaries/releases/download/jdk-${release/+/%2B}/OpenJDK${JRE_PINNED_FEATURE}U-jre_${arch}_linux_hotspot_${release/+/_}.tar.gz"
else
# Another feature version is installed once and then left alone; its digest is the
# one Adoptium's API publishes next to the link.
if [ -x "${JRE_DIR}/bin/java" ]; then
ok "JRE already installed at ${JRE_DIR}"
return 0
fi
json="$(meta_get "https://api.adoptium.net/v3/assets/latest/${FELIS_JRE_VERSION}/hotspot?architecture=${arch}&image_type=jre&os=linux&vendor=eclipse")" \
|| die "could not ask the Adoptium API for a Temurin ${FELIS_JRE_VERSION} JRE"
url="$(printf '%s' "$json" | grep -o '"link": *"[^"]*\.tar\.gz"' || true)"
url="${url%%$'\n'*}"
url="${url%\"}"
url="${url##*\"}"
want="$(printf '%s' "$json" | grep -o '"checksum": *"[0-9a-f]\{64\}"' || true)"
want="${want%%$'\n'*}"
want="${want%\"}"
want="${want##*\"}"
release="$(printf '%s' "$json" | grep -o '"release_name": *"jdk-[^"]*"' || true)"
release="${release%%$'\n'*}"
release="${release%\"}"
release="${release##*\"jdk-}"
[ -n "$url" ] && [ -n "$want" ] && [ -n "$release" ] \
|| die "the Adoptium API lists no Temurin ${FELIS_JRE_VERSION} JRE for linux/${arch}"
fi
# A rerun moves the installer's own JRE to the pinned build, which is how a JRE security
# release reaches the proxy: bump the pin, rerun, and install_velocity_service restarts
# the proxy because the JRE's release file changed. A JRE someone else put here (another
# vendor, or pre-staged for an architecture Temurin does not build) is left alone.
if [ -x "${JRE_DIR}/bin/java" ]; then
if grep -qxF "IMPLEMENTOR_VERSION=\"Temurin-${release}\"" "${JRE_DIR}/release" 2>/dev/null; then
ok "Temurin ${release} JRE already installed at ${JRE_DIR}"
return 0
fi
if ! grep -qxF 'IMPLEMENTOR="Eclipse Adoptium"' "${JRE_DIR}/release" 2>/dev/null; then
ok "JRE at ${JRE_DIR} is not a Temurin build this installer put there; left as is"
return 0
fi
log "moving the proxy's JRE to Temurin ${release}"
fi
log "installing Temurin ${release} JRE (${arch}) to ${JRE_DIR}"
local tmp have
tmp="$(mktemp -d)"
remember_temp "$tmp"
curl -fsSL "$url" -o "${tmp}/jre.tar.gz" || die "failed to download the Temurin JRE: ${url}"
mkdir -p "$JRE_DIR"
curl -fsSL --retry 5 --retry-delay 2 "$url" -o "${tmp}/jre.tar.gz" \
|| die "failed to download the Temurin JRE: ${url}"
have="$(sha256sum <"${tmp}/jre.tar.gz" | cut -d' ' -f1)"
[ "$have" = "$want" ] \
|| die "Temurin ${release} JRE (${arch}) hashes to ${have}, expected ${want}; refusing to install it"
# Unpacked beside the live one and swapped in with two renames, so a failed unpack leaves
# the proxy's runtime untouched. The running proxy keeps the files it has open.
rm -rf "${JRE_DIR}.new" "${JRE_DIR}.old"
mkdir -p "${JRE_DIR}.new"
# The tarball has a single versioned top-level directory (jdk-25+36-jre/); strip it so
# the path in the systemd unit never carries a build number.
tar -C "$JRE_DIR" --strip-components=1 -xzf "${tmp}/jre.tar.gz" || die "failed to unpack the JRE"
[ -x "${JRE_DIR}/bin/java" ] || die "unpacked JRE has no bin/java"
ok "JRE at ${JRE_DIR}/bin/java"
tar -C "${JRE_DIR}.new" --strip-components=1 -xzf "${tmp}/jre.tar.gz" || die "failed to unpack the JRE"
[ -x "${JRE_DIR}.new/bin/java" ] || die "unpacked JRE has no bin/java"
[ ! -e "$JRE_DIR" ] || mv "$JRE_DIR" "${JRE_DIR}.old"
mv "${JRE_DIR}.new" "$JRE_DIR"
rm -rf "${JRE_DIR}.old"
ok "JRE at ${JRE_DIR}/bin/java (Temurin ${release})"
}
install_velocity() {
@@ -2039,22 +2196,18 @@ install_velocity() {
log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR} (sha256 ${have})"
atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root
else
log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build"
resolved="$(papermc_latest_jar velocity "$FELIS_VELOCITY_VERSION")" \
|| die "no Velocity build for ${FELIS_VELOCITY_VERSION} (override with FELIS_VELOCITY_VERSION)"
url="${resolved% *}"
want="${resolved##* }"
log "downloading Velocity ${FELIS_VELOCITY_VERSION}"
tmp="$(mktemp "${VELOCITY_DIR}/.velocity.jar.XXXXXX")"
remember_temp "$tmp"
curl -fsSL "$url" -o "$tmp" || die "failed to download Velocity: ${url}"
# The same gate the Via plugins and the fork jar pass: this jar is the proxy every
# player connects through, and Fill already promised its digest in the URL — a
# truncated or tampered download becomes a refusal here, not a proxy that won't boot.
have="$(sha256sum <"$tmp" | cut -d' ' -f1)"
[ "$have" = "$want" ] \
|| die "Velocity ${FELIS_VELOCITY_VERSION} checksum mismatch: got ${have}, expected ${want}"
atomic_install_file "$tmp" "${VELOCITY_DIR}/velocity.jar" 0644 root root
local version="${FELIS_VELOCITY_VERSION:-${VELOCITY_VERSION:-$VELOCITY_LATEST_MINOR}}"
if [ "$FELIS_GAME_STACK" = "pinned" ] && [ "$version" = "${VELOCITY_VERSION:-}" ]; then
url="$VELOCITY_JAR_URL"
want="$VELOCITY_JAR_SHA256"
else
log "resolving the newest Velocity ${version} build"
resolved="$(papermc_latest_jar velocity "$version")" \
|| die "no Velocity build for ${version} (override with FELIS_VELOCITY_VERSION)"
url="${resolved% *}"
want="${resolved##* }"
fi
stage_velocity_jar "$url" "$want" "$version"
fi
install_via_plugins
@@ -2063,6 +2216,29 @@ install_velocity() {
configure_velocity_firewall
}
# stage_velocity_jar installs the stock proxy from $1 unless velocity.jar already hashes to
# $2, so a rerun of the same build neither downloads nor touches the file the proxy runs.
stage_velocity_jar() {
local url="$1" want="$2" version="$3" have="" tmp
[ -f "${VELOCITY_DIR}/velocity.jar" ] && have="$(sha256sum <"${VELOCITY_DIR}/velocity.jar" | cut -d' ' -f1)"
if [ "$have" = "$want" ]; then
ok "Velocity ${version} already staged"
return 0
fi
log "downloading Velocity ${version}"
tmp="$(mktemp "${VELOCITY_DIR}/.velocity.jar.XXXXXX")"
remember_temp "$tmp"
curl -fsSL --retry 5 --retry-delay 2 "$url" -o "$tmp" || die "failed to download Velocity: ${url}"
# The same gate the Via plugins and the fork jar pass: this jar is the proxy every
# player connects through, and the lock file (or Fill's content-addressed URL) names its
# digest — a truncated or tampered download becomes a refusal here, not a proxy that
# won't boot.
have="$(sha256sum <"$tmp" | cut -d' ' -f1)"
[ "$have" = "$want" ] \
|| die "Velocity ${version} checksum mismatch: got ${have}, expected ${want}"
atomic_install_file "$tmp" "${VELOCITY_DIR}/velocity.jar" 0644 root root
}
# felis_internal_ip echoes the felis-api-internal Service ClusterIP. Cluster DNS does not
# resolve from the host, but a Service ClusterIP DOES route from the node (kube-proxy programs
# the host netns) — the same trick the on-node break-glass console uses. The internal face is
+115 -15
View File
@@ -120,32 +120,27 @@ out="$(bash -c '
')"
expect "meta_get gives up after three attempts" "GAVE UP" "$out"
# --- the resolved-Velocity digest gate --------------------------------------------------
# The download must hash to what the content-addressed URL promised, BEFORE
# --- the Velocity digest gate -----------------------------------------------------------
# The download must hash to what the lock (or the content-addressed URL) promised, BEFORE
# atomic_install_file — the same refusal the Via plugins and the fork jar already get.
# The end pattern spells ${VELOCITY_DIR} with dots: escaped braces are literal in gawk
# and mawk but undefined in POSIX awk, and CI's awk is whatever ubuntu ships.
vblock="$(awk '/log "resolving the newest Velocity/,/atomic_install_file "\$tmp" "\$.VELOCITY_DIR.\/velocity\.jar"/' "$BS")"
[ -n "$vblock" ] || { echo "FAIL: no resolved-Velocity install block found in $BS"; exit 1; }
[ "$(printf '%s\n' "$vblock" | wc -l)" -lt 30 ] \
|| { echo "FAIL: the extracted block is not the velocity install -- did its last line move?"; exit 1; }
vblock="$(awk '/^stage_velocity_jar\(\) \{/,/^}/' "$BS")"
[ -n "$vblock" ] || { echo "FAIL: no stage_velocity_jar found in $BS"; exit 1; }
vdir="$(mktemp -d)"
trap 'rm -f "$jar"; rm -rf "$vdir"' EXIT
vwant="$(printf 'stand-in velocity build\n' | sha256sum | cut -d' ' -f1)"
run_velocity_install() { # digest-the-resolver-reports
WANT="$1" VELOCITY_DIR="$vdir" FELIS_VELOCITY_VERSION=3.5.1 bash -c '
run_velocity_install() { # expected-digest
WANT="$1" VELOCITY_DIR="$vdir" bash -c '
die() { printf "DIE: %s\n" "$*"; exit 1; }
log() { printf "LOG: %s\n" "$*"; }
ok() { printf "OK: %s\n" "$*"; }
remember_temp() { :; }
papermc_latest_jar() {
printf "%s %s\n" "https://fill-data.papermc.io/v1/objects/${WANT}/velocity-3.5.1-615.jar" "$WANT"
}
curl() { while [ "$#" -gt 1 ] && [ "$1" != "-o" ]; do shift; done; printf "stand-in velocity build\n" > "$2"; }
atomic_install_file() { printf "INSTALL: %s\n" "$2"; }
'"$vblock"
atomic_install_file() { printf "INSTALL: %s\n" "$2"; cp "$1" "$2"; }
'"$vblock"'
stage_velocity_jar "https://fill-data.papermc.io/v1/objects/${WANT}/velocity-3.5.1-615.jar" "$WANT" 3.5.1'
}
out="$(run_velocity_install deadbeef)"
@@ -158,6 +153,111 @@ esac
out="$(run_velocity_install "$vwant")"
expect "the matching download installs" "INSTALL: ${vdir}/velocity.jar" "$out"
out="$(run_velocity_install "$vwant")"
case "$out" in
*LOG:*|*INSTALL:*) echo "FAIL a rerun of the staged build downloaded it again"; fails=$((fails + 1)) ;;
*"Velocity 3.5.1 already staged"*) echo "PASS a rerun of the staged build leaves velocity.jar alone" ;;
*) echo "FAIL stage_velocity_jar died on a staged build: $out"; fails=$((fails + 1)) ;;
esac
ivblock="$(awk '/^install_velocity\(\) \{/,/^}/' "$BS")"
run_velocity_choice() { # FELIS_GAME_STACK FELIS_VELOCITY_VERSION lock-version
FELIS_GAME_STACK="$1" FELIS_VELOCITY_VERSION="$2" VELOCITY_VERSION="$3" VELOCITY_LATEST_MINOR=3.5.1 \
VELOCITY_JAR_URL=https://fill-data.papermc.io/v1/objects/aaa/velocity-3.5.1-615.jar VELOCITY_JAR_SHA256=aaa \
FELIS_VELOCITY_FORK_JAR= bash -c '
set -Eeuo pipefail
log() { :; }
die() { printf "DIE: %s\n" "$*"; exit 1; }
install_jre() { :; }
prepare_velocity_layout() { :; }
install_via_plugins() { :; }
write_velocity_config() { :; }
install_velocity_service() { :; }
configure_velocity_firewall() { :; }
papermc_latest_jar() { printf "https://fill-data.papermc.io/v1/objects/bbb/velocity-%s-700.jar bbb\n" "$2"; }
stage_velocity_jar() { printf "STAGE %s %s %s\n" "$@"; }
'"$ivblock"'
install_velocity'
}
expect "a pinned install stages the lock's Velocity build" \
"STAGE https://fill-data.papermc.io/v1/objects/aaa/velocity-3.5.1-615.jar aaa 3.5.1" "$(run_velocity_choice pinned '' 3.5.1)"
expect "another FELIS_VELOCITY_VERSION resolves that minor's newest build" \
"STAGE https://fill-data.papermc.io/v1/objects/bbb/velocity-3.6.0-700.jar bbb 3.6.0" "$(run_velocity_choice pinned 3.6.0 3.5.1)"
expect "FELIS_GAME_STACK=latest resolves the newest build of the default minor" \
"STAGE https://fill-data.papermc.io/v1/objects/bbb/velocity-3.5.1-700.jar bbb 3.5.1" "$(run_velocity_choice latest '' 3.5.1)"
# --- the game-stack lock ----------------------------------------------------------------
# bootstrap.sh reads deploy/game-stack.lock as data: known keys only, all of them present,
# values limited to URL and version characters, digests shaped like digests.
lblock="$(awk '/^load_game_stack_lock\(\) \{/,/^}/' "$BS")"
[ -n "$lblock" ] || { echo "FAIL: no load_game_stack_lock found in $BS"; exit 1; }
lkeys="$(grep '^GAME_STACK_LOCK_KEYS=' "$BS")"
[ -n "$lkeys" ] || { echo "FAIL: no GAME_STACK_LOCK_KEYS in $BS"; exit 1; }
ldir="$(mktemp -d)"
run_lock() { # lock-file
LOCK="$1" bash -c '
set -Eeuo pipefail
die() { printf "DIE: %s\n" "$*"; exit 1; }
'"$lkeys"'
'"$lblock"'
load_game_stack_lock "$LOCK"
printf "MC=%s LIMBO=%s VELOCITY=%s\n" "$MC_VERSION" "$LIMBO_JAR_URL" "$VELOCITY_JAR_SHA256"'
}
repo_lock="$(dirname "$BS")/game-stack.lock"
out="$(run_lock "$repo_lock")"
expect "the shipped lock loads" "MC=$(sed -n 's/^MC_VERSION=//p' "$repo_lock") LIMBO=https://ci.loohpjames.com/job/Limbo/" "$out"
{ cat "$repo_lock"; printf 'EVIL=$(touch /tmp/pwned)\n'; } > "$ldir/unknown"
expect "an unknown key is refused" "DIE: $ldir/unknown: unknown key EVIL" "$(run_lock "$ldir/unknown")"
sed 's|^LIMBO_VERSION=.*|LIMBO_VERSION=$(id)|' "$repo_lock" > "$ldir/subst"
expect "a value with shell syntax is refused" "DIE: $ldir/subst: LIMBO_VERSION has an unexpected value" "$(run_lock "$ldir/subst")"
grep -v '^LUCKPERMS_JAR_SHA256=' "$repo_lock" > "$ldir/missing"
expect "a missing key is refused" "DIE: $ldir/missing does not set LUCKPERMS_JAR_SHA256" "$(run_lock "$ldir/missing")"
sed 's|^PAPER_JAR_SHA256=.*|PAPER_JAR_SHA256=ABCDEF|' "$repo_lock" > "$ldir/badsha"
expect "a malformed digest is refused" "DIE: $ldir/badsha: PAPER_JAR_SHA256 is not a lowercase sha256" "$(run_lock "$ldir/badsha")"
expect "no lock file is refused with the way out" "FELIS_GAME_STACK=latest" "$(run_lock "$ldir/none")"
rm -rf "$ldir"
# --- the Temurin JRE pin ----------------------------------------------------------------
jblock="$(awk '/^install_jre\(\) \{/,/^}/' "$BS")"
[ -n "$jblock" ] || { echo "FAIL: no install_jre found in $BS"; exit 1; }
jdir="$(mktemp -d)"
jtar="$(mktemp -d)"
mkdir -p "$jtar/jdk-25.0.9+1-jre/bin"
printf '#!/bin/sh\n' > "$jtar/jdk-25.0.9+1-jre/bin/java"
chmod +x "$jtar/jdk-25.0.9+1-jre/bin/java"
printf 'IMPLEMENTOR="Eclipse Adoptium"\nIMPLEMENTOR_VERSION="Temurin-25.0.9+1"\n' > "$jtar/jdk-25.0.9+1-jre/release"
tar -C "$jtar" -czf "$jtar/jre.tar.gz" "jdk-25.0.9+1-jre"
jsha="$(sha256sum < "$jtar/jre.tar.gz" | cut -d' ' -f1)"
run_jre() { # machine pinned-sha
MACHINE="$1" SHA="$2" TARBALL="$jtar/jre.tar.gz" JRE_DIR="$jdir/jre" FELIS_JRE_VERSION=25 \
JRE_PINNED_FEATURE=25 JRE_PINNED_RELEASE=25.0.9+1 JRE_PINNED_SHA256_X64="$2" JRE_PINNED_SHA256_AARCH64="$2" bash -c '
set -Eeuo pipefail
die() { printf "DIE: %s\n" "$*"; exit 1; }
log() { printf "LOG: %s\n" "$*"; }
ok() { printf "OK: %s\n" "$*"; }
remember_temp() { :; }
uname() { printf "%s\n" "$MACHINE"; }
curl() { local prev=""; while [ "$#" -gt 1 ] && [ "$1" != "-o" ]; do prev="$1"; shift; done; printf "URL %s\n" "$prev" >&2; cp "$TARBALL" "$2"; }
'"$jblock"'
install_jre' 2>&1
}
out="$(run_jre x86_64 deadbeef)"
expect "a JRE download with the wrong digest is refused" "hashes to ${jsha}, expected deadbeef" "$out"
[ -e "$jdir/jre" ] && { echo "FAIL a refused JRE was unpacked"; fails=$((fails + 1)); } || echo "PASS a refused JRE is not unpacked"
out="$(run_jre aarch64 "$jsha")"
expect "the pinned JRE is downloaded from its GitHub release" \
"URL https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.9%2B1/OpenJDK25U-jre_aarch64_linux_hotspot_25.0.9_1.tar.gz" "$out"
expect "the pinned JRE installs" "OK: JRE at $jdir/jre/bin/java (Temurin 25.0.9+1)" "$out"
expect "a rerun of the pinned JRE is a no-op" "OK: Temurin 25.0.9+1 JRE already installed" "$(run_jre x86_64 "$jsha")"
printf 'IMPLEMENTOR="Eclipse Adoptium"\nIMPLEMENTOR_VERSION="Temurin-25.0.1+8"\n' > "$jdir/jre/release"
out="$(run_jre x86_64 "$jsha")"
expect "an older installer-managed JRE moves to the pin" "LOG: moving the proxy's JRE to Temurin 25.0.9+1" "$out"
expect "the moved JRE is the pinned build" 'IMPLEMENTOR_VERSION="Temurin-25.0.9+1"' "$(cat "$jdir/jre/release")"
printf 'IMPLEMENTOR="Azul Systems, Inc."\n' > "$jdir/jre/release"
expect "a JRE someone else installed is left alone" "is not a Temurin build this installer put there" "$(run_jre x86_64 "$jsha")"
expect "an unsupported architecture keeps a pre-staged JRE" "OK: JRE already installed at $jdir/jre" "$(run_jre riscv64 "$jsha")"
rm -rf "$jdir" "$jtar"
# --- [[auth_source]] carry-forward -----------------------------------------------------
# write_felis_toml regenerates felis.toml wholesale on every run; this is what keeps the
+23
View File
@@ -0,0 +1,23 @@
# The upstream builds this release installs. deploy/bootstrap.sh reads this file (the
# default FELIS_GAME_STACK=pinned), downloads exactly these artifacts and refuses any whose
# sha256 differs, so every host installing one release gets the same login gate, lobby,
# plain-Paper image and proxy, and a rerun rebuilds nothing that did not change.
#
# MC_VERSION is the protocol the whole stack speaks: Limbo speaks exactly one, and Paper
# follows it so a client that passes the login gate can also reach the lobby.
#
# Refresh with deploy/update-game-stack-lock.sh, which resolves upstream's newest builds and
# hashes them. Plain KEY=value lines only; bootstrap reads it without evaluating it.
MC_VERSION=26.3
LIMBO_VERSION=2026.0.3-ALPHA
LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/76/artifact/target/Limbo-2026.0.3-ALPHA-26.3.jar
LIMBO_JAR_SHA256=a2de91fcaa2255aed8a111b8786f370213423c7798eee778c00d016d83aa65d2
LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/76/artifact/spawn.schem
LIMBO_SCHEM_SHA256=70c85dae2db157971ef513e318820c5a5e10a96b813b70e21f8af2b632cbcbc7
PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/49399919246cbf443efc8507447dc948eb7477c41be560b0e87e2a455aff824a/paper-26.3-40.jar
PAPER_JAR_SHA256=49399919246cbf443efc8507447dc948eb7477c41be560b0e87e2a455aff824a
LUCKPERMS_JAR_URL=https://download.luckperms.net/1672/bukkit/loader/LuckPerms-Bukkit-5.5.85.jar
LUCKPERMS_JAR_SHA256=dc637ce18f48d3b75a7ffd1784b85be16a627359090dfe5adf15dab6d145dc7d
VELOCITY_VERSION=3.5.1
VELOCITY_JAR_URL=https://fill-data.papermc.io/v1/objects/b4e3164df5377346854dc6cb9e6a78022b1946ff69e89676313f5f6f1c6f0fb3/velocity-3.5.1-615.jar
VELOCITY_JAR_SHA256=b4e3164df5377346854dc6cb9e6a78022b1946ff69e89676313f5f6f1c6f0fb3
+20 -7
View File
@@ -10,10 +10,11 @@
# There is no bundled server.properties — Limbo writes a default on first run.
# So the runtime is assembled from those two URLs (not a zip) via --build-arg:
#
# . <(grep '^LIMBO_' deploy/game-stack.lock)
# docker build -f deploy/limbo/Dockerfile \
# --build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
# --build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
# --build-arg LIMBO_VERSION=<maven-api-version> \
# --build-arg LIMBO_JAR_URL="$LIMBO_JAR_URL" --build-arg LIMBO_JAR_SHA256="$LIMBO_JAR_SHA256" \
# --build-arg LIMBO_SCHEM_URL="$LIMBO_SCHEM_URL" --build-arg LIMBO_SCHEM_SHA256="$LIMBO_SCHEM_SHA256" \
# --build-arg LIMBO_VERSION="$LIMBO_VERSION" \
# -t felis-limbo:demo .
#
# Note LIMBO_VERSION (the maven API version the plugin compiles against, e.g.
@@ -33,7 +34,7 @@
# JDK 17 fails to read them with "wrong version 65.0, should be 61.0". The image
# also provides the `gradle` binary (this tree vendors no Gradle wrapper).
# build.gradle still targets release 17 bytecode so the plugin loads on Java 17+.
FROM gradle:8.14-jdk21 AS plugin
FROM gradle:8.14-jdk21@sha256:5c4c0c4284de4a19951e82ac78f86dbcda2e136644bbfe159beba7ea3420cc80 AS plugin
WORKDIR /src
# Copy what the limbo module needs: its own tree plus the shared link core it
# srcDir-includes (../shared/src/main/java → /src/plugins/shared/src/main/java), so
@@ -50,21 +51,33 @@ RUN cd plugins/limbo \
# 21-jre: the Limbo jar is Java 21 bytecode (class-file major 65), so a Java 17
# JRE cannot run it (UnsupportedClassVersionError). A 21 JRE also runs the
# plugin's release-17 bytecode fine.
FROM eclipse-temurin:21-jre
FROM eclipse-temurin:21-jre@sha256:49e21e16e3c86eb7816a44a67549910ed090fbeb40c29c525d58bf5e02e91b0f
ARG LIMBO_JAR_URL
ARG LIMBO_JAR_SHA256
ARG LIMBO_SCHEM_URL
ARG LIMBO_SCHEM_SHA256
WORKDIR /limbo
# Pull the two loose LOOHP/Limbo CI artifacts: the server jar (required, saved as
# Limbo.jar) and the default spawn schematic (optional). Fail loudly if the jar
# URL was not supplied.
# Limbo.jar) and the default spawn schematic (optional). Each is checked against the
# digest deploy/game-stack.lock names (bootstrap.sh passes it): the login gate is the
# first thing every player's connection reaches, and Limbo's CI publishes no digest of
# its own.
RUN set -eu; \
if [ -z "${LIMBO_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg LIMBO_JAR_URL=<Limbo server jar> is required" >&2; exit 1; \
fi; \
if [ -z "${LIMBO_JAR_SHA256:-}" ]; then \
echo "ERROR: --build-arg LIMBO_JAR_SHA256=<Limbo jar sha256> is required" >&2; exit 1; \
fi; \
if [ -n "${LIMBO_SCHEM_URL:-}" ] && [ -z "${LIMBO_SCHEM_SHA256:-}" ]; then \
echo "ERROR: --build-arg LIMBO_SCHEM_SHA256=<spawn.schem sha256> is required with LIMBO_SCHEM_URL" >&2; exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
curl -fSL "$LIMBO_JAR_URL" -o /limbo/Limbo.jar; \
echo "$LIMBO_JAR_SHA256 /limbo/Limbo.jar" | sha256sum -c; \
if [ -n "${LIMBO_SCHEM_URL:-}" ]; then \
curl -fSL "$LIMBO_SCHEM_URL" -o /limbo/spawn.schem; \
echo "$LIMBO_SCHEM_SHA256 /limbo/spawn.schem" | sha256sum -c; \
fi; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
mkdir -p /limbo/plugins
+17 -11
View File
@@ -5,13 +5,13 @@
# the POST-auth /menu hub: it is reached only when the login gate transfers an
# authenticated player onward, and it must never be a fallback target.
#
# Build (deploy/bootstrap.sh does this for you; the PAPER_JAR_URL comes from PaperMC's
# Fill v3 API — api.papermc.io v2 has returned HTTP 410 since 2026-07-01):
# Build (deploy/bootstrap.sh does this for you, with the URLs and digests
# deploy/game-stack.lock names):
# . <(grep -E '^(PAPER|LUCKPERMS)_' deploy/game-stack.lock)
# docker build -f deploy/lobby/Dockerfile \
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-26.2-<build>.jar \
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
# --build-arg LUCKPERMS_JAR_URL="$(curl -fsSL https://metadata.luckperms.net/data/all \
# | grep -o 'https://download.luckperms.net/[^"]*/bukkit/loader/[^"]*\.jar')" \
# --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" --build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
# --build-arg LUCKPERMS_JAR_URL="$LUCKPERMS_JAR_URL" \
# --build-arg LUCKPERMS_JAR_SHA256="$LUCKPERMS_JAR_SHA256" \
# -t felis-lobby:demo .
# docker save felis-lobby:demo | sudo k3s ctr images import -
# # felis.toml → [velocity] lobby_image = "felis-lobby:demo"
@@ -28,7 +28,7 @@
# ---- build the felis-paper plugin jar (Paper API is Java 21) ----
# gradle:8.14-jdk21 — an official Gradle image on JDK 21 (this tree vendors no Gradle
# wrapper, and a bare JDK image ships no `gradle`). JDK 21 matches the Paper API.
FROM gradle:8.14-jdk21 AS plugin
FROM gradle:8.14-jdk21@sha256:5c4c0c4284de4a19951e82ac78f86dbcda2e136644bbfe159beba7ea3420cc80 AS plugin
WORKDIR /src
COPY plugins/paper/ ./plugins/paper/
COPY plugins/shared/ ./plugins/shared/
@@ -41,7 +41,7 @@ RUN cd plugins/paper \
# 25-jre, not 21: Paper 26.2 declares `java.version.minimum = 25` (PaperMC Fill v3,
# GET /v3/projects/paper/versions/26.2) and refuses to boot on anything older. A 25 JRE
# also runs the plugin's Java-21 bytecode, so only the runtime moves.
FROM eclipse-temurin:25-jre
FROM eclipse-temurin:25-jre@sha256:bb036ed6cfdc57e3da7c22634d15f1b840d2caf76183861c80e81ca4b5104abb
ARG PAPER_JAR_URL
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
# that shape at build time. Checking the digest after the download turns a truncated or
@@ -51,10 +51,12 @@ ARG PAPER_JAR_SHA256
# (internal/api/handlers_access.go) issues `lp user ...` over RCON, so a lobby built
# without it answers every grant with "Unknown command" — a failure the operator only
# discovers in production, because the server itself starts and runs perfectly well.
# Failing the build is the cheap place to notice. Resolved by URL rather than pinned
# here for the same reason PAPER_JAR_URL is: bootstrap.sh asks upstream for the current
# build, so this file does not go stale on every LuckPerms release.
# Failing the build is the cheap place to notice. Passed in rather than pinned here for
# the same reason PAPER_JAR_URL is: deploy/game-stack.lock names the build, so this file
# does not change on every LuckPerms release. The digest is required like Paper's; the
# jar runs inside the lobby with the server's full permissions.
ARG LUCKPERMS_JAR_URL
ARG LUCKPERMS_JAR_SHA256
WORKDIR /paper
RUN set -eu; \
if [ -z "${PAPER_JAR_URL:-}" ]; then \
@@ -66,11 +68,15 @@ RUN set -eu; \
if [ -z "${LUCKPERMS_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg LUCKPERMS_JAR_URL=<luckperms bukkit jar> is required" >&2; exit 1; \
fi; \
if [ -z "${LUCKPERMS_JAR_SHA256:-}" ]; then \
echo "ERROR: --build-arg LUCKPERMS_JAR_SHA256=<luckperms jar sha256> is required" >&2; exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
mkdir -p /paper/plugins; \
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c; \
curl -fSL "$LUCKPERMS_JAR_URL" -o /paper/plugins/LuckPerms.jar; \
echo "$LUCKPERMS_JAR_SHA256 /paper/plugins/LuckPerms.jar" | sha256sum -c; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
echo "eula=true" > /paper/eula.txt
COPY --from=plugin /felis-paper.jar /paper/plugins/felis-paper.jar
+5 -5
View File
@@ -14,11 +14,11 @@
# image a user brings is made joinable the same way. If the initContainer is absent (no
# FELIS_IMAGE configured) Paper boots as a standalone online server: degraded, not broken.
#
# Build (deploy/bootstrap.sh does this for you; PAPER_JAR_URL comes from PaperMC's Fill v3
# API — the SAME url the lobby build resolves, so this reuses it and adds no new dependency):
# Build (deploy/bootstrap.sh does this for you, with the SAME Paper build the lobby uses —
# deploy/game-stack.lock names it — so this adds no new dependency):
# . <(grep '^PAPER_' deploy/game-stack.lock)
# docker build -f deploy/paper/Dockerfile \
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-<ver>-<build>.jar \
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
# --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" --build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
# -t felis-paper:demo .
# docker save felis-paper:demo | sudo k3s ctr images import -
# # felis.toml → recommended via 0019_recommended_paper.sql (no [velocity] key points here)
@@ -29,7 +29,7 @@
# 25-jre, not 21: Paper 26.2 declares java.version.minimum=25 (PaperMC Fill v3) and refuses
# to boot on anything older.
FROM eclipse-temurin:25-jre
FROM eclipse-temurin:25-jre@sha256:bb036ed6cfdc57e3da7c22634d15f1b840d2caf76183861c80e81ca4b5104abb
ARG PAPER_JAR_URL
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
# that shape at build time. Checking the digest after the download turns a truncated or
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Refreshes deploy/game-stack.lock to upstream's newest builds, hashed.
#
# bash deploy/update-game-stack-lock.sh # rewrite the lock in place
# bash deploy/update-game-stack-lock.sh --check # exit 1 if upstream moved on
#
# It runs the same resolver bootstrap.sh uses for FELIS_GAME_STACK=latest (the functions are
# lifted out of bootstrap.sh, so the two cannot drift), then pins Velocity's newest build of
# VELOCITY_LATEST_MINOR. Limbo and LuckPerms publish no digest, so their jars are downloaded
# and hashed here; Paper and Velocity come from Fill's content-addressed URLs.
#
# Review the diff before committing: MC_VERSION moves the login gate's protocol, and the
# lobby, plain-Paper image and every client follow it.
set -Eeuo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BS="${here}/bootstrap.sh"
LOCK="${here}/game-stack.lock"
check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) printf 'usage: %s [--check]\n' "$0" >&2; exit 2 ;;
esac
log() { printf '[lock] %s\n' "$*" >&2; }
ok() { printf '[ ok ] %s\n' "$*" >&2; }
warn() { :; }
die() { printf '[fail] %s\n' "$*" >&2; exit 1; }
lift() { # function-name
local body
body="$(awk -v f="$1" '$0 ~ "^" f "\\(\\) \\{" {on=1} on {print} on && /^}/ {exit}' "$BS")"
[ -n "$body" ] || die "bootstrap.sh no longer defines $1"
eval "$body"
}
for fn in meta_get papermc_latest_jar luckperms_latest_jar url_sha256 resolve_latest_game_jars; do
lift "$fn"
done
eval "$(grep '^VELOCITY_LATEST_MINOR=' "$BS")"
[ -n "${VELOCITY_LATEST_MINOR:-}" ] || die "bootstrap.sh no longer sets VELOCITY_LATEST_MINOR"
resolve_latest_game_jars
log "resolving the newest Velocity ${VELOCITY_LATEST_MINOR} build"
velocity="$(papermc_latest_jar velocity "$VELOCITY_LATEST_MINOR")" \
|| die "no Velocity build for ${VELOCITY_LATEST_MINOR}"
VELOCITY_VERSION="$VELOCITY_LATEST_MINOR"
VELOCITY_JAR_URL="${velocity% *}"
VELOCITY_JAR_SHA256="${velocity##* }"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
# The comment header is kept as it is; only the KEY=value lines are regenerated.
sed -n '/^#/p;/^#/!q' "$LOCK" > "$tmp"
for key in MC_VERSION LIMBO_VERSION LIMBO_JAR_URL LIMBO_JAR_SHA256 LIMBO_SCHEM_URL LIMBO_SCHEM_SHA256 \
PAPER_JAR_URL PAPER_JAR_SHA256 LUCKPERMS_JAR_URL LUCKPERMS_JAR_SHA256 \
VELOCITY_VERSION VELOCITY_JAR_URL VELOCITY_JAR_SHA256; do
printf '%s=%s\n' "$key" "${!key}" >> "$tmp"
done
if cmp -s "$tmp" "$LOCK"; then
ok "game-stack.lock already pins upstream's newest builds"
exit 0
fi
diff -u "$LOCK" "$tmp" >&2 || true
if [ "$check" = 1 ]; then
die "upstream has newer builds than game-stack.lock"
fi
cp "$tmp" "$LOCK"
ok "game-stack.lock updated; run go test . and the bootstrap tests, then commit"