Skip to content

Build scripts ​

release.mjs — git-flow releases ​

npm run release -- start <version> / hotfix <version> / finish: branches, version bumps across every workspace, the merges and the pushes; the Release workflow tags v<version> from the version on main. See Releasing.

admin-limits.mjs — an account's limits from a terminal ​

npm run admin:limits -- --list, -- <account id>, -- <account id> [--builds N] [--rooms N] [--creations N] [--budget-exempt | --no-budget-exempt] [--until YYYY-MM-DD] [--note "…"] or -- <account id> --reset [--note "…"]: what the admin page (?admin) does, as an admin (FOI_ADMINS) of the API at FOI_API_URL (http://localhost:8080 by default) with their session in FOI_API_TOKEN. A flag sets its own limit and keeps the others the override had (--builds default gives one back its default). Plain Node; admin-limits.test.ts runs it against the API in its process. See Operations.

steam-build.mjs — the game's builds on Steam ​

node build/steam-build.mjs [--windows <folder>] [--linux <folder>] [--mac <folder or .app>] [--out <folder>] [--branch <beta>] [--upload] writes steamcmd's build files into dist/steam: an app build (app_build_<app>.vdf, going live on the beta branch STEAM_BRANCH names, never default) and a depot build per platform (depot_build_<depot>.vdf) from the release's unpacked folders (apps/client/release/win-unpacked, linux-unpacked, the mac* folder holding Five On It.app, unless given; one file, an AppImage, works too), for the app FOI_STEAM_APP_ID and the depots STEAM_DEPOT_WINDOWS, STEAM_DEPOT_LINUX and STEAM_DEPOT_MAC (a platform without its depot or its build is left out, and said so). By default a dry run: it prints the files and uploads nothing. --upload runs steamcmd +login $STEAM_BUILD_USER +run_app_build … +quit (STEAMCMD names steamcmd, the build account's login kept by steamcmd or STEAM_BUILD_PASSWORD). Plain Node; steam-build.test.ts checks the files and the command. The Release workflow's steam job runs it, by hand and approved (Releasing).

pack-server.mjs — the server bundle ​

npm run pack:server bundles the game server into dist/five-on-it-server-<version>.tgz (one dependency-free server.mjs, a package.json, a README). The Release workflow attaches it to the GitHub Release.

docs-versions.mjs — the published documentation's index ​

The docs site's versions.json worked out as a version is published (.github/workflows/docs.yml): a release goes under its minor (--slug-of 0.77.2 prints 0.77), the published version replaces its slug's entry, the latest is the newest minor (an older one published again leaves it where it was), and every minor older than the latest's ten predecessors is to be pruned. It prints the latest, then the folders to prune. docs-versions.test.ts pins the order, the latest, the gaps and the prunes (Releasing).

pack-stack.mjs — the stack bundle ​

npm run pack:stack writes dist/five-on-it-stack-<version>.tgz: deploy/compose.yaml without its build: sections (the images are pulled; made from the one file so the two never drift), the Caddyfile, postgres/init.sql, an .env.example naming this release's images (FOI_IMAGES, FOI_VERSION; FOI_IMAGES from the environment names another registry) and a README. It checks what it wrote: no build: left, the four images named by the two. The Release workflow attaches it to the GitHub Release, after the stack came up on its images and passed its smoke.

bundle-server.mjs — the server bundle itself ​

node build/bundle-server.mjs <outfile>: the server, the simulation, the protocol, the physics and the npm dependencies in one ES module (esbuild). pack-server.mjs uses it, and so does the client build, which ships the file as dist-electron/server.mjs so the Electron app can host a game.

editor.mjs — the Babylon.js Editor on this machine ​

Levels are authored in the Babylon.js Editor, a desktop app npm cannot install. node build/editor.mjs check runs after npm install (postinstall, skipped in CI) and prints where to download the version the project is built for (the babylonjs-editor-tools pin of apps/level-editor/package.json: the installer for this platform from the editor's update feed, or the download page) when no editor is found; it finds the editor at BABYLONJS_EDITOR_PATH or where its installer puts it. npm run level:editor (node build/editor.mjs open) starts the editor on apps/level-editor/project.bjseditor, passing the file twice: the installed editor reads its third argument, as electron . <file> lays it out in its own development setup.

loc.mjs — the repo size counter ​

npm run loc (part of npm run validate and CI) counts non-blank lines in git-tracked source files, per folder and in two buckets, product and tests (*.test.*, tests/**, testing.ts, test_*.py). It never fails on size: there is no budget. The count is there to keep scope visible: every estimate in the Technical specs (§11) is stated in these lines, so growth is a decision and not a surprise. Every run rewrites the badges in badges/ (version.svg, loc.svg, loc-sim.svg, loc-tests.svg: static SVGs in the shields.io style, drawn locally so they render on a private repository too) that the README embeds with relative links; commit them with the change so the badges show the counted numbers.

What counts: only files listed by git ls-files (so generated output is excluded by construction, and a new file counts once it is added), only code extensions (.ts .tsx .mts .cts .js .mjs .cjs .jsx .html .css .py), blank lines skipped. Python counts because the Blender kits, models and level generators under assets/blender are product code (the level generator runs on the servers' Blender workers). Markdown, JSON and YAML are config and docs, not code.

perf.mjs — the frame-rate benchmark ​

npm run perf builds the web client for production (into reports/perf/web, served by a tiny static server of its own), starts a game server on main-riverbend per scenario (empty, city, chaos, storm-night and riverbend; test-buildings for buildings, test-city-life for city-life) and drives headless Chromium on this machine's GPU (--renderer swiftshader for the software rasterizer CI draws with): after a warm-up it times every frame for 20 s (frame times, long tasks, the game's own frame report from window.__perf(), the server's step times from /health), then profiles the page (and, with --server-profile, the server through Node's inspector) for 8 s. The report goes to reports/perf/ (git-ignored) as JSON and Markdown, the table to the console. Not part of validate: it needs a GPU and a few minutes. Ports 7783 (game), 5193 (page) and 9333 (inspector). See Performance.

e2e-plan.mjs — the test-plan gate ​

npm run e2e-plan (part of validate) parses the coverage table of tests/e2e/TEST-PLAN.md and the { tag: [...] } arrays of every spec under tests/e2e/specs, then checks that every implemented row has exactly one test tagged with its @E2E-nn id, every tagged test has a row, the tier tag matches the row, and each test carries exactly one tier (@smoke, @core, @long), at least one area (@app, @drive, @net, @ai) and exactly one plan id. It holds the gate to its budget (tests/e2e/support/lanes.ts): no @core test recorded over 20 s on the self-hosted runner but a test level's only one, every test-* level with a @core test, and the core tier packed into 12 minutes in the self-hosted runner's five lanes alone. It takes the levels, the level of a spec and the server ports from tests/e2e/support/levels.ts, the module the suite runs on.

env-check.mjs — the environment gate ​

npm run env:check (part of validate) holds the example env files to the code: every environment variable the code, the stack and the scripts read is listed in exactly one of them (.env.example for running the apps, the tools and the tests on their own, deploy/.env.example for the Compose stack, deploy/fleet/.env.fleet.example for the fleet scripts), with a comment above it, and every name they list is read by something. It scans the repository's files by the idioms they read with (process.env.X, an env record's env.X and env['X'], names destructured from one, the quoted names a file hands its helpers when it reads a record by a computed key; Python's os.environ; Compose's ${X}; Caddy's {$X}; a Dockerfile's ARG and ENV; the Git hooks' $X), and env-check.json names what the platform, CI or a test runner provides (with a reason each), what a tool reads rather than the code, and the files it skips. The names the fleet passes on to the stack (STACK_PASSED, STACK_SHARED in deploy/fleet/settings.mjs) may be in both of their files. env-check.test.ts covers every scanner and failure. See Quality gates.

e2e-lanes.mjs — a machine's lanes side by side ​

npm run test:e2e:lanes -- [--run core|long|all] [--lanes <n>] packs a run of the browser's tiers into n lanes on this machine (3 by default) by the tests' recorded seconds (tests/e2e/support/lanes.ts, tests/e2e/timings.json) and runs them at once; on CI, --machines <name:lanes:speed,...> --machine <k> packs the run over every machine of it and runs machine k's lanes, and --budget-minutes fails it past that. Each lane is one Playwright process on ports of its own (E2E_PORT_OFFSET, the lane times 1000, which tests/e2e/support/levels.ts adds to every port of the suite; its Vite keeps a dependency cache of its own) with the game servers of its files' levels only, writing its blob report, a JSON report of its tests' seconds and its results in folders of its own; at the end the blob reports are gathered in tests/e2e/blobs and, off CI, merged into the HTML report. See Quality gates.

e2e-route.mjs — where a run goes ​

The route job of .github/workflows/e2e.yml runs it: from the repository variables (E2E_RUNNER and E2E_LANES, E2E_GPU_RUNNER with E2E_GPU_JOBS, E2E_GPU_LANES and E2E_GPU_SPEED, E2E_HOSTED_RUNNER and E2E_HOSTED_JOBS), the run (E2E_RUN) and the self-hosted runner's status (E2E_STATUS: online, offline or unknown), it plans the run's machines (the self-hosted runner and GitHub's GPU runners, else GitHub's runners in software) and writes them to $GITHUB_OUTPUT for the matrix. Run it with those variables to see a plan.

e2e-timings.mjs — the tests' recorded seconds ​

npm run e2e:timings -- <report>... rewrites tests/e2e/timings.json from a run's reports: the lanes' JSON reports (the e2e-timings-* artifacts of a CI run, gh run download <run> -p 'e2e-timings-*') or a job's log. Files the run did not cover keep their seconds; files that are gone are dropped.

sast.mjs — static security scan ​

npm run sast runs npm audit --omit=dev --audit-level=high (blocking) and a full npm audit (advisory), then Semgrep with the community TypeScript/Node/secrets rulesets, using the local semgrep binary when present and the semgrep/semgrep Docker image otherwise. Only ERROR-severity findings fail the run; reports go to reports/sast/. Tests are excluded through .semgrepignore. Runs in .github/workflows/sast.yml.

dast.mjs — dynamic security scan ​

npm run dast starts the game server on DAST_PORT (default 7790), waits for /health, runs the OWASP ZAP baseline scan from ghcr.io/zaproxy/zaproxy:stable against it and stops the server. zap-rules.tsv decides which rules are FAIL, WARN or IGNORE. Reports go to reports/dast/. Runs weekly in .github/workflows/dast.yml. The HTTP surface is only /health today; the gate exists so the API app inherits a working scan when it arrives.