feat(drill): the staging drill, the week before each forced wipe (stage 9c, D307-D309, D317-D319)

A Gitea job that checks each new Rust staging build in the seven days before the
monthly forced wipe:

- tools/drill/static.sh: staging's managed assemblies (DepotDownloader) and
  Oxide's staging build; whether Oxide has caught up (fieldlist --oxide-behind),
  the swap's field list compared, and the plugin compiled.
- tools/drill/drill.js: window (D318), gate (one check per depot manifest),
  live (rust-staging runs rnt.run all; rust-carbon is stopped and restarted when
  both 6000-map rigs run, D309) and report (one issue per build, state on the
  drill branch; a build waits up to three days for Oxide, D317).
- tools/drill/rig-start.sh: the rig's startup; installs the branch and Oxide's
  build for it, and refuses to start a mismatched pair.
- RunicNpcTest 0.7.2: stage 5's five fights look for a field with room for their
  280 m line (D319), and the spawn check names what did not spawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-07 01:29:45 -05:00
parent 9b316c6266
commit c25f73c525
9 changed files with 1058 additions and 8 deletions

View File

@@ -0,0 +1,149 @@
# The staging drill (docs/runicnpc/PLAN.md stage 9c, D307-D309, D317, D318).
#
# Rust changes under RunicNPC on every forced wipe, and what breaks first is the subclass swap and
# the brain (README "The update surface"). Facepunch publishes each build on Steam's `staging`
# branch days before it ships. The forced wipe is monthly, the first Thursday, so the job does
# something only in the seven days before it, which leaves a week to fix what it finds (D318). It
# is scheduled daily and stops at its first step outside that week. In the week, it checks every
# new staging build:
#
# window (tools/drill/drill.js window): the week before a forced wipe? (A run by hand always is.)
# static half (tools/drill/static.sh, about a minute, no server):
# Rust's managed assemblies from Steam's staging branch and Oxide's staging build →
# has Oxide caught up · the swap's field list regenerated and compared · RunicNPC compiled.
# gate (tools/drill/drill.js gate): stop here when this build (the manifest of Rust's Linux
# depot) was already checked.
# live half (drill.js live): the `rust-staging` server installs the same pair at start, then
# RunicNPC, Kits and the test harness are loaded and `rnt.run all` runs. When both 6000-map rigs
# are running, `rust-carbon` is stopped for it and started again (D309). Skipped while Oxide is
# behind Rust: an Oxide server on that pair dies at boot.
# report (drill.js report): anything that fails or differs opens ONE issue for the build, labelled
# `staging-drill` (a later run on the same build comments on it). The result is kept on the
# `drill` branch (drill.json), like the installer's `bundles` branch.
#
# While Oxide's staging build is behind Rust's, a build waits up to three days, opening nothing;
# then it is reported as "Oxide has not caught up" (D317).
#
# Run by hand with `branch: public` to rehearse it on a pair known to match: it reports in the job
# summary only, and changes no state and opens no issue.
#
# The schedule runs only from the default branch (`main`), so it starts with the stage 9 cutover;
# before then, run it by hand on `edge`.
#
# Secrets (Settings → Actions → Secrets, the organisation's):
# PTERODACTYL_DRILL_KEY — the panel client key made for this job alone (D309). Never printed.
# REGISTRY_USER / REGISTRY_TOKEN — as release.yml: push the `drill` branch and open issues.
name: Staging drill
on:
schedule:
- cron: '23 10 * * *'
workflow_dispatch:
inputs:
branch:
description: 'Steam branch to drill (public = a rehearsal: job summary only)'
default: staging
force:
description: 'Check the build again even if it was checked'
type: boolean
default: false
concurrency:
group: staging-drill
cancel-in-progress: false
env:
GITEA_HOST: gitea.whitlocktech.com
REPO: RunicGateway/runicnpc-rust
jobs:
drill:
runs-on: ubuntu-latest
# Rust's update, a 3500 map's boot and navmesh (about 18 minutes when a new Rust build
# regenerates the map), and the harness: under an hour.
timeout-minutes: 180
env:
BRANCH: ${{ github.event.inputs.branch || 'staging' }}
WORK: ${{ github.workspace }}/drill-work
steps:
- uses: actions/checkout@v4
# 22 or later: drill.js talks to the panel's console over the global WebSocket.
- uses: actions/setup-node@v4
with:
node-version: 22
- name: The week before a forced wipe?
id: window
run: node tools/drill/drill.js window ${{ github.event_name == 'workflow_dispatch' && '--force' || '' }}
# tools/fieldlist and the compiler (Roslyn's csc.dll ships in the SDK).
- name: .NET SDK
if: ${{ steps.window.outputs.open == 'true' }}
run: |
set -euo pipefail
curl -sSL -o "$RUNNER_TEMP/dotnet-install.sh" https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh
bash "$RUNNER_TEMP/dotnet-install.sh" --channel 9.0 --install-dir "$HOME/.dotnet" > /dev/null
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV"
- name: The drill branch (the last build checked)
if: ${{ steps.window.outputs.open == 'true' }}
run: |
set -euo pipefail
git config user.name "runicnpc-ci"
git config user.email "ci@whitlocktech.com"
rm -rf drill-state
git worktree prune
if git ls-remote --exit-code --heads origin drill > /dev/null 2>&1; then
git fetch origin drill
git worktree add -B drill drill-state origin/drill
else
# First run: a root commit with an empty tree, so the branch inherits no code history.
ROOT="$(git commit-tree "$(git hash-object -t tree /dev/null)" -m 'chore(drill): start the drill branch')"
git worktree add -B drill drill-state "$ROOT"
fi
[ -f drill-state/drill.json ] || echo '{}' > drill-state/drill.json
cat drill-state/drill.json
- name: Static half
if: ${{ steps.window.outputs.open == 'true' }}
run: bash tools/drill/static.sh
- name: A build not checked yet?
id: gate
if: ${{ steps.window.outputs.open == 'true' }}
run: node tools/drill/drill.js gate --state drill-state/drill.json --work "$WORK" ${{ github.event.inputs.force == 'true' && '--force' || '' }}
- name: Live half (rust-staging)
id: live
if: ${{ steps.gate.outputs.run == 'true' }}
continue-on-error: true
env:
PANEL_KEY: ${{ secrets.PTERODACTYL_DRILL_KEY }}
run: node tools/drill/drill.js live --work "$WORK" --branch "$BRANCH"
- name: Report
id: report
if: ${{ always() && steps.gate.outputs.run == 'true' }}
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: node tools/drill/drill.js report --state drill-state/drill.json --work "$WORK"
- name: Keep the result on the drill branch
if: ${{ always() && steps.gate.outputs.run == 'true' && env.BRANCH == 'staging' }}
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
git remote set-url origin "https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
cd drill-state
git add -A
if git diff --cached --quiet; then echo "no change"; exit 0; fi
node -e "const s=require('./drill.json').lastRun||{};console.log('chore(drill): '+(s.manifest||'?')+' '+(s.outcome||'')+' [skip ci]')" > ../drill-msg.txt
git commit -q -F ../drill-msg.txt
git push origin drill