ci(bundle): compose schema 2 for ServUO and Rust beside schema 1
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m12s
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m12s
Module-rust phase 18, step 2 of docs/modules/rust/PLAN.md §34.2.7 (D146, D147). The compose job now writes three streams on the bundles branch: current.json schema 1, ServUO, exactly as before v2/servuo/ schema 2, game "servuo" v2/rust/ schema 2, game "rust" (Rust-Link + Rust-Plugins) The gates move into .gitea/scripts/compose-bundles.sh, so they can be run by hand against the real release API or a mock without a runner. Each game composes independently: a Rust failure still lets a ServUO bundle publish, and the run goes red afterwards. A game whose repos have never released composes nothing and does not fail, which is where Rust is until its cutover. A ServUO matrix carries the same tag at both schemas, so the first run publishes v2/servuo/bundle-2026.09.15.json rather than a second name for the pair. Schema 1 stops being composed on 2027-01-01 and is left frozen. Tested in debian:12 against the real releases (v2/servuo published as 2026.09.15, schema 1 unchanged, Rust skipped; rerun is a no-op; past the retirement date schema 1 is frozen) and against a mock Gitea for Rust (document composed, protocol mismatch refused, missing manifest key refused, rerun a no-op, ServUO failing independently). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
437
.gitea/scripts/compose-bundles.sh
Executable file
437
.gitea/scripts/compose-bundles.sh
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compose every bundle stream from the latest component releases.
|
||||
#
|
||||
# Called by .gitea/workflows/bundle.yml, which owns checkout, publishing and the
|
||||
# stale-component check; this script owns the gates and the documents. It lives
|
||||
# in a file rather than inline so it can be run by hand against the real release
|
||||
# API — which is how it is tested — without a runner.
|
||||
#
|
||||
# ── The streams (docs/modules/rust/PLAN.md §34.2.2, D146/D147) ───────────────
|
||||
#
|
||||
# current.json, bundle-<tag>.json schema 1, ServUO (until SCHEMA1_RETIRES)
|
||||
# v2/servuo/current.json, bundle-<tag>.json schema 2, game "servuo"
|
||||
# v2/rust/current.json, bundle-<tag>.json schema 2, game "rust"
|
||||
#
|
||||
# A schema-2 document names ONE game. The two games release on their own
|
||||
# schedules, and a document naming both would hand a ServUO host a new bundle
|
||||
# every time a Rust plugin shipped.
|
||||
#
|
||||
# Schema 1 is still composed because every installer already in the field reads
|
||||
# only `current.json` and refuses any schema but 1. After SCHEMA1_RETIRES it is
|
||||
# left FROZEN at its last bundle rather than deleted, so an old installer still
|
||||
# resolves something and every bundle-<tag>.json stays pinnable.
|
||||
#
|
||||
# ── One matrix, one tag ──────────────────────────────────────────────────────
|
||||
# A ServUO matrix published at both schemas carries the SAME tag in both, so a
|
||||
# `--bundle <tag>` an operator wrote down means one pair whichever installer
|
||||
# reads it. The first run after schema 2 lands is the case this is for: the
|
||||
# ServUO matrix has not changed, so v2/servuo takes the tag schema 1 already
|
||||
# gave it rather than inventing a second name for the same pair.
|
||||
#
|
||||
# ── Failure is per game ──────────────────────────────────────────────────────
|
||||
# A Rust release with a protocol mismatch must not stop a ServUO bundle from
|
||||
# publishing, and the reverse. Each game composes in its own subshell; what
|
||||
# succeeded is written, and the script exits 1 at the end if anything failed, so
|
||||
# the run is still red.
|
||||
#
|
||||
# A game none of whose components has released yet composes nothing, and that is
|
||||
# NOT a failure — it is the state Rust is in until its first cutover.
|
||||
#
|
||||
# ── Interface ────────────────────────────────────────────────────────────────
|
||||
# PUBLISHED the bundles-branch worktree (read, and written on change)
|
||||
# WORK scratch directory
|
||||
# GITEA_HOST default gitea.whitlocktech.com (GITEA_BASE overrides the whole URL)
|
||||
# TODAY YYYY-MM-DD, default today UTC (tests override it)
|
||||
# SCHEMA1_RETIRES YYYY-MM-DD, default 2027-01-01 (PLAN.md §34.4)
|
||||
#
|
||||
# Results, for the workflow:
|
||||
# $WORK/result.env changed=true|false, commit_subject=…
|
||||
# $WORK/summary.md the job summary's body
|
||||
# $WORK/stale.tsv <repo slug>\t<released tag>, one per resolved component
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${PUBLISHED:?PUBLISHED must name the bundles-branch worktree}"
|
||||
: "${WORK:?WORK must name a scratch directory}"
|
||||
GITEA_HOST="${GITEA_HOST:-gitea.whitlocktech.com}"
|
||||
# Overridable so the script can be pointed at a mock Gitea (how the Rust path was
|
||||
# tested before either Rust repository had released).
|
||||
GITEA_BASE="${GITEA_BASE:-https://${GITEA_HOST}}"
|
||||
TODAY="${TODAY:-$(date -u +%Y-%m-%d)}"
|
||||
SCHEMA1_RETIRES="${SCHEMA1_RETIRES:-2027-01-01}"
|
||||
|
||||
SERVUO_LINK_REPO="RunicGateway/link"
|
||||
SERVUO_OVERLAY_REPO="RunicGateway/servuo-plugins"
|
||||
RUST_LINK_REPO="RunicGateway/Rust-Link"
|
||||
RUST_PLUGIN_REPO="RunicGateway/Rust-Plugins"
|
||||
|
||||
# Fixed top-level directories inside each payload tarball. Deliberately NOT
|
||||
# versioned — a versioned prefix would mean parsing the version out of a path in
|
||||
# order to read the manifest that declares the version.
|
||||
SERVUO_OVERLAY_PREFIX="runicgateway-overlay"
|
||||
RUST_PLUGIN_PREFIX="runicgateway-rust-plugin"
|
||||
|
||||
mkdir -p "$WORK" "$PUBLISHED"
|
||||
: > "$WORK/summary.md"
|
||||
: > "$WORK/stale.tsv"
|
||||
: > "$WORK/published.txt"
|
||||
|
||||
fail() { echo "::error::$*" >&2; exit 1; }
|
||||
note() { printf -- '%s\n' "$*" >> "$WORK/summary.md"; }
|
||||
|
||||
# ── Release resolution ───────────────────────────────────────────────────────
|
||||
# Read ANONYMOUSLY, on purpose: these are exactly the requests the shipped
|
||||
# installer and the egg make, from a host with no Gitea credentials. A repo
|
||||
# flipped to private fails CI here instead of on an operator's machine.
|
||||
#
|
||||
# Returns 0 with $WORK/<key>-release.json written, or 2 when the repo has never
|
||||
# released (Gitea answers /releases/latest with 404). Anything else is fatal.
|
||||
resolve_latest() {
|
||||
local slug="$1" key="$2" code tag
|
||||
code="$(curl -sS -o "$WORK/${key}-release.json" -w '%{http_code}' \
|
||||
"${GITEA_BASE}/api/v1/repos/${slug}/releases/latest")" || fail "could not reach ${slug}'s releases"
|
||||
case "$code" in
|
||||
200) ;;
|
||||
404) return 2 ;;
|
||||
*) fail "${slug} /releases/latest answered HTTP ${code}" ;;
|
||||
esac
|
||||
tag="$(jq -r '.tag_name' "$WORK/${key}-release.json")"
|
||||
[ -n "$tag" ] && [ "$tag" != "null" ] || fail "${slug}'s latest release has no tag"
|
||||
printf '%s\t%s\n' "$slug" "$tag" >> "$WORK/stale.tsv"
|
||||
echo "==> ${slug} latest: ${tag}"
|
||||
}
|
||||
|
||||
release_tag() { jq -r '.tag_name' "$WORK/$1-release.json"; }
|
||||
|
||||
# ── GATE 2 (installer PLAN.md §7.1): assets exist, checksums match ───────────
|
||||
# Every asset a bundle will reference is downloaded and verified against the
|
||||
# SHA256SUMS published beside it. SHA256SUMS is the trust anchor for these
|
||||
# deliberately UNSIGNED artifacts, and the installer and the egg verify against
|
||||
# the hashes THIS job records; a hash copied from a file nobody checked would
|
||||
# make the chain decorative.
|
||||
#
|
||||
# `sha256sum -c` catches a SHA256SUMS entry with no asset. The reverse — an asset
|
||||
# with no entry — is checked by name, because -c would pass right over it. The
|
||||
# `\*?` matches sha256sum's binary-mode marker.
|
||||
verify_assets() {
|
||||
local key="$1" dir="$WORK/$1" sums_url name url
|
||||
rm -rf "$dir"; mkdir -p "$dir"
|
||||
sums_url="$(jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url' "$WORK/${key}-release.json")"
|
||||
[ -n "$sums_url" ] && [ "$sums_url" != "null" ] \
|
||||
|| fail "${key} release has no SHA256SUMS asset — nothing to verify against"
|
||||
curl -sSfL -o "${dir}/SHA256SUMS" "$sums_url"
|
||||
|
||||
jq -r '.assets[] | select(.name != "SHA256SUMS") | "\(.name)\t\(.browser_download_url)"' \
|
||||
"$WORK/${key}-release.json" > "${dir}/asset-list.tsv"
|
||||
[ -s "${dir}/asset-list.tsv" ] || fail "${key} release carries no assets besides SHA256SUMS"
|
||||
|
||||
while IFS=$'\t' read -r name url; do
|
||||
[ -n "$name" ] || continue
|
||||
echo " fetching ${key}/${name}"
|
||||
curl -sSfL -o "${dir}/${name}" "$url"
|
||||
grep -qE "[ \t]\*?${name}\$" "${dir}/SHA256SUMS" \
|
||||
|| fail "${key} asset ${name} has no entry in that release's SHA256SUMS"
|
||||
done < "${dir}/asset-list.tsv"
|
||||
|
||||
( cd "$dir" && sha256sum -c SHA256SUMS ) \
|
||||
|| fail "${key} assets do not match the SHA256SUMS published with them"
|
||||
echo "==> ${key}: all assets present and verified"
|
||||
}
|
||||
|
||||
sha_of() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
|
||||
# One asset as a bundle `{name,url,sha256}` object.
|
||||
asset_json() {
|
||||
local key="$1" name="$2" url
|
||||
url="$(awk -F'\t' -v n="$name" '$1 == n { print $2 }' "$WORK/${key}/asset-list.tsv")"
|
||||
jq -n --arg name "$name" --arg url "$url" --arg sha "$(sha_of "$WORK/${key}/${name}")" \
|
||||
'{ name: $name, url: $url, sha256: $sha }'
|
||||
}
|
||||
|
||||
# The single tarball a payload release carries, by name. Exactly one: a second
|
||||
# .tar.gz would leave the installer guessing which one it deploys.
|
||||
single_tarball() {
|
||||
local key="$1" count
|
||||
count="$(awk -F'\t' '$1 ~ /\.tar\.gz$/' "$WORK/${key}/asset-list.tsv" | wc -l)"
|
||||
[ "$count" -eq 1 ] || fail "expected exactly 1 .tar.gz in the ${key} release, found ${count}"
|
||||
awk -F'\t' '$1 ~ /\.tar\.gz$/ { printf "%s", $1 }' "$WORK/${key}/asset-list.tsv"
|
||||
}
|
||||
|
||||
# ── GATE 1 (installer PLAN.md §7.1): both halves speak one protocol ──────────
|
||||
# The sidecar side is PROTOCOL_VERSION in sidecar/src/main.rs, read at the
|
||||
# RELEASE TAG — not from the binary, which would mean executing a downloaded
|
||||
# artifact (and, for older releases, one with no way to answer). Both link and
|
||||
# Rust-Link keep the constant at that path.
|
||||
sidecar_protocol() {
|
||||
local slug="$1" tag="$2" key="$3" p
|
||||
curl -sSfL -o "$WORK/${key}-main.rs" \
|
||||
"${GITEA_BASE}/${slug}/raw/tag/${tag}/sidecar/src/main.rs"
|
||||
p="$(sed -nE 's/^[[:space:]]*pub const PROTOCOL_VERSION[^=]*=[[:space:]]*([0-9]+).*/\1/p' "$WORK/${key}-main.rs" | head -1)"
|
||||
# Empty means the constant moved. "Could not read" must never read as "matches".
|
||||
[ -n "$p" ] || fail "could not read PROTOCOL_VERSION from ${slug}@${tag}:sidecar/src/main.rs — has the constant moved? Gate 1 cannot be skipped."
|
||||
printf '%s' "$p"
|
||||
}
|
||||
|
||||
# Unpack a payload tarball and return the path of its manifest.json, having
|
||||
# checked the manifest names the version it was released under.
|
||||
payload_manifest() {
|
||||
local key="$1" tarball="$2" prefix="$3" tag="$4" m v
|
||||
rm -rf "$WORK/${key}-x"; mkdir -p "$WORK/${key}-x"
|
||||
tar -xzf "$WORK/${key}/${tarball}" -C "$WORK/${key}-x"
|
||||
m="$WORK/${key}-x/${prefix}/manifest.json"
|
||||
[ -f "$m" ] || fail "the ${key} tarball has no ${prefix}/manifest.json — the installer and the egg resolve it at that exact path"
|
||||
v="$(jq -r '.version' "$m")"
|
||||
# A manifest that disagrees with its tag means the release stamped one version
|
||||
# and tagged another; every record of "what is installed" would then be wrong.
|
||||
[ "$v" = "${tag#v}" ] || fail "${key} manifest says version ${v} but the release is tagged ${tag}"
|
||||
printf '%s' "$m"
|
||||
}
|
||||
|
||||
# ── Publishing helpers ───────────────────────────────────────────────────────
|
||||
# `bundle` and `generated` describe the RUN, not the matrix. Comparing them would
|
||||
# make every nightly cron look like a change and commit a dated duplicate of the
|
||||
# same matrix forever, so only what an installer acts on is compared.
|
||||
same_content() {
|
||||
local published="$1" content="$2"
|
||||
[ -f "$published" ] || return 1
|
||||
cmp -s <(jq -S 'del(.bundle, .generated)' "$published") <(jq -S '.' "$content")
|
||||
}
|
||||
|
||||
# A fresh date tag that names no bundle in any of the given directories. Two
|
||||
# bundles on one day get .2, .3, … so a tag always names exactly one matrix.
|
||||
fresh_tag() {
|
||||
local base tag n=1 d taken
|
||||
base="$(date -u -d "$TODAY" +%Y.%m.%d)"
|
||||
tag="$base"
|
||||
while :; do
|
||||
taken=false
|
||||
for d in "$@"; do [ -f "${d}/bundle-${tag}.json" ] && taken=true; done
|
||||
[ "$taken" = false ] && break
|
||||
n=$((n + 1)); tag="${base}.${n}"
|
||||
done
|
||||
printf '%s' "$tag"
|
||||
}
|
||||
|
||||
# Write <dir>/bundle-<tag>.json and <dir>/current.json from a content document.
|
||||
# The header keys go first so a person reading the file sees what it is.
|
||||
publish_doc() {
|
||||
local dir="$1" tag="$2" content="$3"
|
||||
mkdir -p "$dir"
|
||||
jq --arg bundle "$tag" --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'(if has("game") then { schema: .schema, game: .game } else { schema: .schema } end)
|
||||
+ { bundle: $bundle, generated: $generated } + del(.schema, .game)' \
|
||||
"$content" > "${dir}/bundle-${tag}.json"
|
||||
cp "${dir}/bundle-${tag}.json" "${dir}/current.json"
|
||||
local rel="${dir#"$PUBLISHED"}"; rel="${rel#/}"
|
||||
echo "${rel:+${rel}/}bundle-${tag}.json" >> "$WORK/published.txt"
|
||||
}
|
||||
|
||||
# ── ServUO: schema 1 and v2/servuo ───────────────────────────────────────────
|
||||
compose_servuo() {
|
||||
resolve_latest "$SERVUO_LINK_REPO" servuo-link \
|
||||
|| fail "${SERVUO_LINK_REPO} has no published release"
|
||||
resolve_latest "$SERVUO_OVERLAY_REPO" servuo-overlay \
|
||||
|| fail "${SERVUO_OVERLAY_REPO} has no published release"
|
||||
local link_tag overlay_tag
|
||||
link_tag="$(release_tag servuo-link)"; overlay_tag="$(release_tag servuo-overlay)"
|
||||
|
||||
verify_assets servuo-link
|
||||
verify_assets servuo-overlay
|
||||
|
||||
# link's binaries onto platform keys. An unrecognized name is a hard failure,
|
||||
# so a new target in link's release.yml reddens this job instead of silently
|
||||
# vanishing from every bundle; a missing required key fails the same way.
|
||||
local assets='{}' name url plat
|
||||
while IFS=$'\t' read -r name url; do
|
||||
[ -n "$name" ] || continue
|
||||
case "$name" in
|
||||
*-linux-x86_64) plat=linux-x86_64 ;;
|
||||
*-linux-aarch64) plat=linux-aarch64 ;;
|
||||
*-windows-x86_64.exe) plat=windows-x86_64 ;;
|
||||
*) fail "unrecognized link asset '${name}' — teach compose-bundles.sh this name or the bundle would silently omit it." ;;
|
||||
esac
|
||||
assets="$(jq --arg p "$plat" --argjson a "$(asset_json servuo-link "$name")" '. + { ($p): $a }' <<<"$assets")"
|
||||
done < "$WORK/servuo-link/asset-list.tsv"
|
||||
for plat in linux-x86_64 linux-aarch64 windows-x86_64; do
|
||||
jq -e --arg p "$plat" 'has($p)' <<<"$assets" >/dev/null \
|
||||
|| fail "link release is missing a ${plat} binary; the installer ships for all three"
|
||||
done
|
||||
|
||||
local tarball manifest overlay_protocol link_protocol
|
||||
[ "$(wc -l < "$WORK/servuo-overlay/asset-list.tsv")" -eq 1 ] \
|
||||
|| fail "expected exactly 1 overlay asset besides SHA256SUMS"
|
||||
tarball="$(single_tarball servuo-overlay)"
|
||||
manifest="$(payload_manifest servuo-overlay "$tarball" "$SERVUO_OVERLAY_PREFIX" "$overlay_tag")"
|
||||
overlay_protocol="$(jq -r '.protocol' "$manifest")"
|
||||
link_protocol="$(sidecar_protocol "$SERVUO_LINK_REPO" "$link_tag" servuo-link)"
|
||||
echo "==> servuo: sidecar ${link_tag} protocol=${link_protocol} | overlay ${overlay_tag} protocol=${overlay_protocol}"
|
||||
[ "$link_protocol" = "$overlay_protocol" ] || fail \
|
||||
"PROTOCOL MISMATCH — sidecar ${link_tag} speaks ${link_protocol}, overlay ${overlay_tag} declares ${overlay_protocol}. Refusing a bundle whose shard the sidecar rejects with 409. Fix: land the matching half and let its release cut, or bump servuo-plugins/overlay.toml."
|
||||
|
||||
local common
|
||||
common="$(jq -n \
|
||||
--arg link_repo "$SERVUO_LINK_REPO" --arg link_tag "$link_tag" --argjson assets "$assets" \
|
||||
--arg ov_repo "$SERVUO_OVERLAY_REPO" --arg ov_tag "$overlay_tag" \
|
||||
--arg ov_commit "$(jq -r '.commit' "$manifest")" \
|
||||
--arg ov_min "$(jq -r '.servuo.min_version' "$manifest")" \
|
||||
--arg ov_patched "$(jq -r '.servuo.patches_verified_against' "$manifest")" \
|
||||
--argjson ov_asset "$(asset_json servuo-overlay "$tarball")" \
|
||||
--argjson protocol "$link_protocol" \
|
||||
'{ protocol: $protocol,
|
||||
sidecar: { repo: $link_repo, tag: $link_tag, version: ($link_tag | ltrimstr("v")),
|
||||
protocol: $protocol, assets: $assets },
|
||||
overlay: { repo: $ov_repo, tag: $ov_tag, version: ($ov_tag | ltrimstr("v")),
|
||||
commit: $ov_commit, protocol: $protocol,
|
||||
servuo: { min_version: $ov_min, patches_verified_against: $ov_patched },
|
||||
asset: $ov_asset } }')"
|
||||
|
||||
# Schema 1: byte-for-byte the shape every shipped installer parses.
|
||||
jq '{ schema: 1, protocol: .protocol, link: .sidecar, overlay: .overlay }' \
|
||||
<<<"$common" > "$WORK/servuo-s1.json"
|
||||
# Schema 2: the same pair, in the game-neutral shape (§34.2.2). The payload's
|
||||
# `compat` is schema 1's `servuo` block.
|
||||
jq '{ schema: 2, game: "servuo", protocol: .protocol, sidecar: .sidecar,
|
||||
payload: ({ kind: "overlay" } + (.overlay | del(.servuo)) + { compat: .overlay.servuo }) }' \
|
||||
<<<"$common" > "$WORK/servuo-s2.json"
|
||||
|
||||
local s1_active=false s1_changed=false s2_changed=false tag=""
|
||||
[[ "$TODAY" < "$SCHEMA1_RETIRES" ]] && s1_active=true
|
||||
if [ "$s1_active" = true ] && ! same_content "$PUBLISHED/current.json" "$WORK/servuo-s1.json"; then
|
||||
s1_changed=true
|
||||
fi
|
||||
same_content "$PUBLISHED/v2/servuo/current.json" "$WORK/servuo-s2.json" || s2_changed=true
|
||||
|
||||
if [ "$s1_changed" = false ] && [ "$s2_changed" = false ]; then
|
||||
note "- **ServUO**: no change (link \`${link_tag}\`, overlay \`${overlay_tag}\`, protocol ${link_protocol})."
|
||||
[ "$s1_active" = true ] || note " Schema 1 retired on ${SCHEMA1_RETIRES}; its \`current.json\` stays frozen."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# One matrix, one tag: when schema 1 already names this pair, v2 borrows its
|
||||
# tag — unless v2 somehow already holds that tag for different content, since
|
||||
# a published bundle-<tag>.json is never rewritten.
|
||||
if [ "$s1_active" = true ] && [ "$s1_changed" = false ] && [ -f "$PUBLISHED/current.json" ]; then
|
||||
tag="$(jq -r '.bundle' "$PUBLISHED/current.json")"
|
||||
if [ -f "$PUBLISHED/v2/servuo/bundle-${tag}.json" ]; then tag=""; fi
|
||||
fi
|
||||
[ -n "$tag" ] || tag="$(fresh_tag "$PUBLISHED" "$PUBLISHED/v2/servuo")"
|
||||
|
||||
[ "$s1_changed" = true ] && publish_doc "$PUBLISHED" "$tag" "$WORK/servuo-s1.json"
|
||||
[ "$s2_changed" = true ] && publish_doc "$PUBLISHED/v2/servuo" "$tag" "$WORK/servuo-s2.json"
|
||||
local which=""
|
||||
[ "$s1_changed" = true ] && which="schema 1"
|
||||
[ "$s2_changed" = true ] && which="${which:+${which} + }schema 2"
|
||||
note "- **ServUO**: published \`${tag}\` (${which}) — link \`${link_tag}\`, overlay \`${overlay_tag}\`, protocol ${link_protocol}."
|
||||
echo "servuo ${tag}" >> "$WORK/published-games.txt"
|
||||
}
|
||||
|
||||
# ── Rust: v2/rust ────────────────────────────────────────────────────────────
|
||||
compose_rust() {
|
||||
local rc=0 missing=""
|
||||
resolve_latest "$RUST_LINK_REPO" rust-link || { rc=$?; [ "$rc" -eq 2 ] && missing="${RUST_LINK_REPO}"; }
|
||||
rc=0
|
||||
resolve_latest "$RUST_PLUGIN_REPO" rust-plugin || { rc=$?; [ "$rc" -eq 2 ] && missing="${missing:+${missing} and }${RUST_PLUGIN_REPO}"; }
|
||||
if [ -n "$missing" ]; then
|
||||
echo "==> rust: no release yet from ${missing}, so nothing to compose (not a failure)"
|
||||
note "- **Rust**: skipped — no release yet from ${missing}."
|
||||
return 0
|
||||
fi
|
||||
local link_tag plugin_tag
|
||||
link_tag="$(release_tag rust-link)"; plugin_tag="$(release_tag rust-plugin)"
|
||||
|
||||
verify_assets rust-link
|
||||
verify_assets rust-plugin
|
||||
|
||||
# Rust-Link's release: a binary per platform, the egg's launcher, and the egg
|
||||
# itself. The egg is verified (gate 2 covers every asset) but is not part of
|
||||
# the bundle — a panel admin imports it; nothing resolves it from here.
|
||||
local assets='{}' launcher="" name url plat
|
||||
while IFS=$'\t' read -r name url; do
|
||||
[ -n "$name" ] || continue
|
||||
case "$name" in
|
||||
*-linux-x86_64) plat=linux-x86_64 ;;
|
||||
*-windows-x86_64.exe) plat=windows-x86_64 ;;
|
||||
with-sidecar.sh) launcher="$(asset_json rust-link "$name")"; continue ;;
|
||||
egg-*.json) continue ;;
|
||||
*) fail "unrecognized Rust-Link asset '${name}' — teach compose-bundles.sh this name or the bundle would silently omit it." ;;
|
||||
esac
|
||||
assets="$(jq --arg p "$plat" --argjson a "$(asset_json rust-link "$name")" '. + { ($p): $a }' <<<"$assets")"
|
||||
done < "$WORK/rust-link/asset-list.tsv"
|
||||
# No linux-aarch64: RustDedicated has no arm64 build (D149).
|
||||
for plat in linux-x86_64 windows-x86_64; do
|
||||
jq -e --arg p "$plat" 'has($p)' <<<"$assets" >/dev/null \
|
||||
|| fail "Rust-Link release is missing a ${plat} binary (D149)"
|
||||
done
|
||||
[ -n "$launcher" ] || fail "Rust-Link release has no with-sidecar.sh — the egg's startup runs it (§34.2.6)"
|
||||
|
||||
local tarball manifest plugin_protocol link_protocol
|
||||
tarball="$(single_tarball rust-plugin)"
|
||||
manifest="$(payload_manifest rust-plugin "$tarball" "$RUST_PLUGIN_PREFIX" "$plugin_tag")"
|
||||
[ -f "$(dirname "$manifest")/RunicGateway.cs" ] \
|
||||
|| fail "the Rust-Plugins tarball has no ${RUST_PLUGIN_PREFIX}/RunicGateway.cs"
|
||||
plugin_protocol="$(jq -r '.protocol' "$manifest")"
|
||||
link_protocol="$(sidecar_protocol "$RUST_LINK_REPO" "$link_tag" rust-link)"
|
||||
echo "==> rust: sidecar ${link_tag} protocol=${link_protocol} | plugin ${plugin_tag} protocol=${plugin_protocol}"
|
||||
[ "$link_protocol" = "$plugin_protocol" ] || fail \
|
||||
"PROTOCOL MISMATCH — Rust-Link ${link_tag} speaks ${link_protocol}, Rust-Plugins ${plugin_tag} declares ${plugin_protocol}. The game link has no 409: a mismatched plugin would mis-parse. Fix: land the matching half and let its release cut, or bump Rust-Plugins/overlay.toml."
|
||||
|
||||
jq -n \
|
||||
--arg link_repo "$RUST_LINK_REPO" --arg link_tag "$link_tag" \
|
||||
--argjson assets "$assets" --argjson launcher "$launcher" \
|
||||
--arg p_repo "$RUST_PLUGIN_REPO" --arg p_tag "$plugin_tag" \
|
||||
--slurpfile m "$manifest" \
|
||||
--argjson p_asset "$(asset_json rust-plugin "$tarball")" \
|
||||
--argjson protocol "$link_protocol" \
|
||||
'{ schema: 2, game: "rust", protocol: $protocol,
|
||||
sidecar: { repo: $link_repo, tag: $link_tag, version: ($link_tag | ltrimstr("v")),
|
||||
protocol: $protocol, assets: $assets, launcher: $launcher },
|
||||
payload: { kind: "plugin", repo: $p_repo, tag: $p_tag, version: ($p_tag | ltrimstr("v")),
|
||||
commit: $m[0].commit, protocol: $protocol,
|
||||
compat: { frameworks: { oxide: { min_version: $m[0].min_oxide_version },
|
||||
carbon: { min_version: $m[0].min_carbon_version } },
|
||||
requires_plugins: $m[0].requires_plugins },
|
||||
asset: $p_asset } }' > "$WORK/rust-s2.json"
|
||||
# A manifest missing a key would compose a `null` the installer then trusts.
|
||||
jq -e '([.payload.compat.frameworks[].min_version, .payload.commit] | all(type == "string"))
|
||||
and (.payload.compat.requires_plugins | type == "array")' "$WORK/rust-s2.json" >/dev/null \
|
||||
|| fail "the Rust-Plugins manifest is missing commit, a framework floor, or requires_plugins"
|
||||
|
||||
if same_content "$PUBLISHED/v2/rust/current.json" "$WORK/rust-s2.json"; then
|
||||
note "- **Rust**: no change (Rust-Link \`${link_tag}\`, Rust-Plugins \`${plugin_tag}\`, protocol ${link_protocol})."
|
||||
return 0
|
||||
fi
|
||||
local tag
|
||||
tag="$(fresh_tag "$PUBLISHED/v2/rust")"
|
||||
publish_doc "$PUBLISHED/v2/rust" "$tag" "$WORK/rust-s2.json"
|
||||
note "- **Rust**: published \`${tag}\` — Rust-Link \`${link_tag}\`, Rust-Plugins \`${plugin_tag}\`, protocol ${link_protocol}."
|
||||
echo "rust ${tag}" >> "$WORK/published-games.txt"
|
||||
}
|
||||
|
||||
# ── Run both, independently ──────────────────────────────────────────────────
|
||||
: > "$WORK/published-games.txt"
|
||||
FAILED=""
|
||||
for game in servuo rust; do
|
||||
echo "────────── ${game} ──────────"
|
||||
# NOT `if ! ( … )`: a subshell in a condition runs with errexit OFF, so an
|
||||
# unchecked failed download inside it would sail on. Capture the status instead.
|
||||
set +e
|
||||
( set -e; "compose_${game}" )
|
||||
rc=$?
|
||||
set -e
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
FAILED="${FAILED:+${FAILED}, }${game}"
|
||||
note "- **${game}**: **FAILED** — see the log. Nothing was published for it."
|
||||
fi
|
||||
done
|
||||
|
||||
CHANGED=false
|
||||
[ -s "$WORK/published.txt" ] && CHANGED=true
|
||||
{
|
||||
echo "changed=${CHANGED}"
|
||||
echo "commit_subject=chore(bundle): publish $(paste -sd ',' "$WORK/published-games.txt" | sed 's/,/, /g') [skip ci]"
|
||||
} > "$WORK/result.env"
|
||||
|
||||
echo "────────── result ──────────"
|
||||
cat "$WORK/summary.md"
|
||||
[ "$CHANGED" = true ] && { echo "files:"; sed 's/^/ /' "$WORK/published.txt"; }
|
||||
[ -z "$FAILED" ] || fail "compose failed for: ${FAILED}"
|
||||
Reference in New Issue
Block a user