Files
mesh-catalog/modules/mesh-build-node/README.md
T
mesh-tools c7e892ca63
mesh/merge-gate head c7e892ca63b3 merged into trunk 82649bc770d1 — pass: builds mesh-build-go, mesh-build-node, sent nowhere; no bus step; every machine co…
mesh/repo-check head c7e892ca63b3 merged into trunk 82649bc770d1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Mark the build modules moved, so a machine assigned them composes before the forge serves their entry (issue 1537)
2026-10-20 22:59:36 +00:00

69 lines
6.1 KiB
Markdown

# mesh-build-node — the Node and TypeScript build module
Design 47, release R4c-3 (novox/hq ADR 0405, ADR 0466 §1, ADR 0469 §1 and §4). The module that builds and checks
TypeScript code on a build machine, as an account of its own (`runs-as`, no login shell, home at mode 0700), with the
packages it declares (`nodejs`, `npm`, `git`, `openssh`) and the capability `user-namespaces`. The bundle is Go
(`cmd/mesh-build-node`). It claims no seat: it is assigned to the build machines (ADR 0469 §6) and runs dark until the
builder uses it. Its forge key and ssh files are the same as mesh-build-go's (issue 1484), filled on the node by the
provisioner's client (issue 1537).
## The recipe
`recipe.go` names the one Node version it builds with (`RecipeNode`, `v26.10.0`, as `node --version` prints it) and the
recipe's own version (`RecipeSteps`). The recipe version is a hash of those two and of the bytes of the lock file
`toolchain/package-lock.json`, so one changed byte of the lock, another Node or another recipe moves it.
## The toolchain directory
`toolchain/package.json` and its committed `package-lock.json` name typescript 5.9.3, esbuild 0.25.12, @types/node 22.20.1 (the types tsc checks node imports against),
@novox/mesh-sdk 0.1.14 and nats 2.29.3. The mesh unpacks both, as the archive artifact `toolchain`, into `<home>/toolchain/` (resource `toolchain-dir`; a
manifest names no host, and the lock does, so it is not a template); the program reads the lock from there. When `<cache dir>/toolchain/<recipe version>` is missing
the module runs `npm ci --ignore-scripts` there once (a directory without its `.installed` mark counts as missing).
**How the registry is reached** (the choice of issue 1488): the lock holds @novox/mesh-sdk at the forge's public
address, and npm 12 installs a locked tarball only from the registry it is configured with. The module therefore
requires `npm-package-registry` (read access, provided by gitea) and, like the lab module, takes its binding and sealed
secret as files in its home. The install gets a private npm config file with the `@novox` scope at the binding's
`public-root` (else scheme, address, port and npm path) and the bound account's credential, and removes it afterwards;
every other package resolves from the public registry. This is the way today's build agent names it.
The runtime tree for bundles with externals (the pruned sdk and nats today in `/app/runtime`) is not installed
separately: sdk and nats are in this directory's `node_modules`, and the build step (issue 1490) takes them from there.
## The guard and the health
At start, every ten minutes, before every step and at every health look, the module reads the lock again and the
installed Node (`node --version`) and writes it with the recipe version to `<node>.typescript` of its `toolchains`
state. A machine whose Node is not the recipe's refuses every step, the step ending `refused` on its key in `steps` with
both versions named, and `build_node_health` (the module's `runtime-health`) answers unhealthy with the same text, so
the controller raises the machine's condition. A step runs only after the toolchain directory is installed.
## Tools
`build_node_status` and `build_node_health` answer in their call. The step tools run behind the guard and the
toolchain install: a long step answers at once with its id and ends on `<node>.<id>` of `steps` (ADR 0469 §2).
- `build_node_fetch` {repository, commit, ref} (issue 1489): the commit over ssh as the module's account
(`buildstep.FetchCommit`, remote `ssh://mesh-forge/<owner>/<name>.git`) into `<work>/<id>/tree`, then for each
TypeScript module (a module.json with a typescript artifact) the packages its package.json names beyond the SDK,
`typescript` and `@types/*` (the toolchain carries those): none installs nothing; some need a package-lock.json, and
are installed as the build agent does (scratch copy without the SDK,
devDependencies and scripts; `npm ci --omit=dev --omit=peer --ignore-scripts --no-bin-links`; copies of the SDK
removed). The mesh's registry reaches `npm ci` through a private config file (`--userconfig`), so
other `@novox/` packages install. A module with packages and no lock does not fail the fetch: its result lists it
under `refused` with the reason, and a build of that bundle fails naming it (issue 1459). Every command runs through `forgekey.ExecRunnerClean(0)` with PATH, HOME, the locale and its own
words, nothing of this process's environment. Workspaces older than a day are removed at each fetch.
- `build_node_test` {workspace, module, level} (issue 1491): the unit level is the type-check (the toolchain's tsc, the
package's `tsconfig.json` or the build agent's flags over its .ts files) and the `test` script; the integration level
is the `test:integration` script, the type-check being its setup. Bounds (the Go module's; ADR 0445 names none for
TypeScript): unit 5 s, integration 30 s setup and 60 s running. The test script finds `tsc` and `esbuild` on its PATH through `<work>/<id>/bin` (shell scripts calling the toolchain's). A level over its bound says `over_bound` and passes;
a failed one finishes the step with `passed` false and the output (the step fails only when the tests could not be run). Neither script nor .ts files: no tests. The toolchain makes no bin
links, so a script runs `node` and files, not `tsc` by name.
- `build_node_build` {workspace, module, artifact} (issue 1490): the build agent's tsc and esbuild lines
(mesh-controller internal/builder, cited in `build.go`), the launchers, `package.json`, `buildstep.Pack`, and the
digest. No network. The build runs in a staging copy of the module at `<work>/<id>/stage-<artifact>/modules/module`, with the
toolchain's packages (not typescript or esbuild) copied to `stage-<artifact>/node_modules`: the build agent's depth
(`/app/modules/module`, `/app/node_modules`), so esbuild's `../../node_modules/...` path comments, and the bundle's
digest, equal its. Copies, not links, because the mesh makes no symlink. A bundle that keeps an external gets them in its own `node_modules`.
The packed archive is written beside the tree as `<artifact>.tar.gz`.