Files
runicnpc-rust/tools/drill/static.sh
wtclaude e5548472da
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 29s
fix(cutover): gate 1 asks Steam when public was set, not the manifest's date or staging (D332)
The 2026-11-05 weekend never cut over: gate 1 blocked every hourly run. Both of
its checks rested on assumptions the wipe disproved:

- The wipe build went live Thursday evening, but its depot manifest is dated
  11-04: the date DepotDownloader prints is when Facepunch built it.
- Public never names staging's manifest. Facepunch promotes a hidden `release`
  branch ("Release, before public"), and staging had moved on before the wipe.

Gate 1 now reads Steam's app info (steamcmd +app_info_print 258550, kept by
static.sh as appinfo.txt with BRANCH=public): branches.public.timeupdated must be
on the wipe day or later, and the Linux depot manifest Steam names for public
must be the one the static half downloaded. The staging static half is gone from
edge's cutover.yml; --staging-work is still accepted, since main's copy passes it
until the cutover merge carries the new wiring over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HJEmgsXtLR5kLfh7f9y8P
2026-11-06 04:37:50 -06:00

178 lines
8.0 KiB
Bash

#!/bin/bash
# The staging drill's static half (docs/runicnpc/PLAN.md D308). Never shipped.
#
# tools/drill/static.sh (WORK=drill-work BRANCH=staging by default)
#
# Needs no server. It downloads only RustDedicated_Data/Managed/*.dll from a Steam branch, and
# Oxide's build for that branch, and:
#
# 1. asks whether Oxide's build has caught up with Rust's (tools/fieldlist --oxide-behind). Until it
# has, an Oxide server on that branch dies at boot, so the drill waits for it (D317);
# 2. regenerates the swap's field list (tools/fieldlist, D232) from Rust's own Assembly-CSharp.dll
# and compares it with the one plugin/RunicNPC.cs carries;
# 3. compiles plugin/RunicNPC.cs against those assemblies with Oxide's laid over them, as an Oxide
# server would. Oxide's patched Assembly-CSharp.dll is needed: the plugin uses members Rust
# ships non-public (IAISenses, for one).
#
# It writes $WORK/static.json and, beside it, oxide-behind.txt, fields.diff and compile.txt. In
# static.json, oxideBehind is a count (-1: not measured), fields "same"/"differs", compile
# "ok"/"errors", and either is "error" when its step could not run. The script exits 0 whenever it
# ran; what it found is in static.json. DEPOTDOWNLOADER and DOTNET point at other copies (on
# Windows, DEPOTDOWNLOADER=…/DepotDownloader.exe from Git Bash).
set -uo pipefail
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
WORK=${WORK:-$ROOT/drill-work}
BRANCH=${BRANCH:-staging}
if [ "$BRANCH" = public ]; then
OXIDE_URL=${OXIDE_URL:-https://github.com/OxideMod/Oxide.Rust/releases/latest/download/Oxide.Rust-linux.zip}
else
OXIDE_URL=${OXIDE_URL:-https://downloads.oxidemod.com/artifacts/Oxide.Rust/$BRANCH/Oxide.Rust-linux.zip}
fi
DD_URL=https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_3.4.0/DepotDownloader-linux-x64.zip
DOTNET=${DOTNET:-dotnet}
mkdir -p "$WORK"
rm -rf "$WORK/depot" "$WORK/oxide" "$WORK/refs" "$WORK/fields"
rm -f "$WORK"/static.json "$WORK"/oxide-behind.txt "$WORK"/fields.diff "$WORK"/compile.txt "$WORK"/appinfo.txt
managed=error
behind=-1
fields=error
compile=error
note=""
oxide_modified=""
manifest=""
manifest_date=""
steam_note=""
finish() {
printf '{"branch":"%s","manifest":"%s","manifestDate":"%s","managed":"%s","oxideBehind":%s,"fields":"%s","compile":"%s","oxideModified":"%s","note":"%s","steamNote":"%s"}\n' \
"$BRANCH" "$manifest" "$manifest_date" "$managed" "$behind" "$fields" "$compile" "$oxide_modified" "$note" "$steam_note" > "$WORK/static.json"
cat "$WORK/static.json"
exit 0
}
# Git Bash does not translate paths inside a response file; elsewhere this is the path as it is.
native() { if command -v cygpath > /dev/null; then cygpath -m "$1"; else printf '%s' "$1"; fi; }
# ---- When Steam set the branch to its build (BRANCH=public only: the cutover's gate 1) ----
# The depot manifest's date below is when Facepunch BUILT the build, not when it went live (the
# 2026-11-05 wipe build was dated 11-04). Steam's app info says when the branch was SET to it
# (depots.branches.<branch>.timeupdated) and which Linux depot manifest that is. steamcmd's whole
# output goes to appinfo.txt; tools/drill/cutover.js reads it. A failure only leaves steamNote set,
# and gate 1 blocks. STEAMCMD points at another steamcmd (steamcmd.sh, or steamcmd.exe on Windows).
steam_info() {
local sc=${STEAMCMD:-} i
if [ -z "$sc" ]; then
mkdir -p "$WORK/steamcmd"
curl -sSL --fail https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz | tar -xz -C "$WORK/steamcmd" \
|| { steam_note="steamcmd could not be fetched"; return; }
sc=$WORK/steamcmd/steamcmd.sh
fi
# steamcmd updates itself on its first start and then may print nothing, so it gets three tries.
# It is 32-bit: a bare runner image lacks its libgcc, which is installed after the first miss.
for i in 1 2 3; do
"$sc" +login anonymous +app_info_update 1 +app_info_print 258550 +quit > "$WORK/appinfo.txt" 2>&1
grep -q '^"258550"' "$WORK/appinfo.txt" && return
if [ "$i" = 1 ] && command -v apt-get > /dev/null && [ "$(id -u)" = 0 ]; then
{ apt-get update -qq && apt-get install -y -qq lib32gcc-s1; } > "$WORK/steamcmd-deps.txt" 2>&1
fi
done
steam_note="steamcmd printed no app info: $(tail -2 "$WORK/appinfo.txt" | tr '\n"\\' ' ')"
}
if [ "$BRANCH" = public ]; then
steam_info
fi
# ---- Rust's managed assemblies, from Steam (no login: the dedicated server is anonymous) ----
DD=${DEPOTDOWNLOADER:-}
if [ -z "$DD" ]; then
curl -sSL --fail -o "$WORK/dd.zip" "$DD_URL" && unzip -oq "$WORK/dd.zip" -d "$WORK/dd" && chmod +x "$WORK/dd/DepotDownloader" \
|| { note="DepotDownloader could not be fetched"; finish; }
DD=$WORK/dd/DepotDownloader
fi
printf 'regex:^RustDedicated_Data/Managed/[^/]+\\.dll$\n' > "$WORK/files.txt"
"$DD" -app 258550 -depot 258552 -branch "$BRANCH" -filelist "$WORK/files.txt" -dir "$WORK/depot" -os linux \
> "$WORK/depot.txt" 2>&1
MANAGED=$WORK/depot/RustDedicated_Data/Managed
# Which build this is, from Steam itself: the Linux depot's manifest and its date. (api.steamcmd.net
# answered a build id a day stale on 2026-10-06, so the drill does not ask it.)
manifest=$(tr -d '\r' < "$WORK/depot.txt" | sed -n 's/^Manifest \([0-9]*\) (\(.*\))$/\1/p' | head -1)
manifest_date=$(tr -d '\r' < "$WORK/depot.txt" | sed -n 's/^Manifest \([0-9]*\) (\(.*\))$/\2/p' | head -1)
if [ ! -f "$MANAGED/Assembly-CSharp.dll" ]; then
note="no Assembly-CSharp.dll from Steam's $BRANCH branch: $(tail -3 "$WORK/depot.txt" | tr '\n"' ' ')"
finish
fi
managed=ok
# ---- Oxide's build for the branch ----
oxide_modified=$(curl -sSIL "$OXIDE_URL" | tr -d '\r' | sed -n 's/^[Ll]ast-[Mm]odified: //p' | tail -1)
if ! { curl -sSL --fail -o "$WORK/oxide.zip" "$OXIDE_URL" && unzip -oq "$WORK/oxide.zip" -d "$WORK/oxide"; }; then
note="Oxide's $BRANCH build could not be downloaded"
finish
fi
OXIDE_MANAGED=$WORK/oxide/RustDedicated_Data/Managed
# The tool is built once, here, so a build failure is not mistaken for anything it reports.
if ! "$DOTNET" build "$ROOT/tools/fieldlist" -c Release -o "$WORK/fieldlist" > "$WORK/fieldlist-build.txt" 2>&1; then
note="tools/fieldlist did not build: $(grep -m1 'error' "$WORK/fieldlist-build.txt" | tr '"' ' ')"
finish
fi
FIELDLIST=("$DOTNET" "$WORK/fieldlist/FieldList.dll")
# ---- 1. Has Oxide caught up? ----
if "${FIELDLIST[@]}" --oxide-behind "$MANAGED" "$OXIDE_MANAGED/Assembly-CSharp.dll" > "$WORK/oxide-behind.txt" 2>&1; then
behind=$(sed -n 's/^behind \([0-9]*\)$/\1/p' "$WORK/oxide-behind.txt" | head -1)
behind=${behind:--1}
fi
# ---- 2. The swap's field list (D232) ----
mkdir -p "$WORK/fields"
cp "$ROOT/plugin/RunicNPC.cs" "$WORK/fields/RunicNPC.cs"
block() { sed -n '/\/\/ <fieldlist>/,/\/\/ <\/fieldlist>/p' "$1" | grep -v "Generated by tools/fieldlist" | tr -d '\r'; }
if "${FIELDLIST[@]}" "$MANAGED" "$WORK/fields/RunicNPC.cs" > "$WORK/fields/run.txt" 2>&1; then
if diff -u --label carried <(block "$ROOT/plugin/RunicNPC.cs") --label "$BRANCH" <(block "$WORK/fields/RunicNPC.cs") > "$WORK/fields.diff"; then
fields=same
else
fields=differs
fi
else
cp "$WORK/fields/run.txt" "$WORK/fields.diff"
fi
# ---- 3. Compile against the branch with Oxide's build laid over ----
mkdir -p "$WORK/refs"
cp "$MANAGED"/*.dll "$WORK/refs/"
cp "$OXIDE_MANAGED"/*.dll "$WORK/refs/"
# Oxide.References.dll bundles its own older copy of Newtonsoft.Json, which Oxide's compiler never
# references; with it, every JObject is ambiguous.
rm -f "$WORK/refs/Oxide.References.dll"
DOTNET_DIR=$(dirname "$(readlink -f "$(command -v "$DOTNET")")")
CSC=$(ls "$DOTNET_DIR"/sdk/*/Roslyn/bincore/csc.dll 2>/dev/null | tail -1)
if [ -z "$CSC" ]; then
note="no csc.dll under $DOTNET_DIR/sdk"
finish
fi
# A response file: some 250 references outgrow a Windows command line.
for f in "$WORK/refs"/*.dll; do printf '"-r:%s"\n' "$(native "$f")"; done > "$WORK/refs.rsp"
if "$DOTNET" "$CSC" -nologo -noconfig -nostdlib -target:library -nowarn:1701,1702 -warn:0 \
-out:"$WORK/RunicNPC.dll" "@$WORK/refs.rsp" "$ROOT/plugin/RunicNPC.cs" > "$WORK/compile.txt" 2>&1; then
compile=ok
else
compile=errors
fi
finish