Compare commits

...
Author SHA1 Message Date
mesh-admin ea73e713f5 i3status-rust: name the pacman-contrib package apart from the update script, and expect it in the test (issue 628)
mesh/merge-gate out of date: judged against trunk cd239283207a, the trunk is now 0d723484a84a; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk cd239283207a, the trunk is now 0d723484a84a; judged again on a recheck or when it is merged
mesh/delivery checking
The package took the id `updates`, which the update script already holds, so the node-engine refused two resources with one identity.
2026-10-13 11:59:31 +00:00
mesh-admin e79197d5a1 i3status-rust, docker-compose: each module declares the packages it needs (issue 628)
mesh/merge-gate out of date: judged against trunk 19ecad261cf2, the trunk is now 8922c890595b; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 19ecad261cf2, the trunk is now 8922c890595b; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
i3status-rust left pacman-contrib to the pacman module because two modules declaring one package was once refused; ADR 0303 allows it, so the bar declares what its update block runs. docker-compose's README said the package goes with the module, which the node-engine never does.
2026-10-13 11:52:38 +00:00
mesh-admin 19ecad261c Merge pull request 'lab: install each checkout where its lock file is, check out mesh-tools-go, and have lab_check catch both (issue 624)' (#268) from fix/624-lab-run-installs-what-each-checkout-has into main 2026-10-13 11:11:58 +00:00
jochen c6a9c52dc7 Install each lab checkout where its lock file is, check out mesh-tools-go, and have lab_check catch both (hq issue 624)
mesh/merge-gate head c6a9c52dc7f2 merged into trunk 45d01e77c26d — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head c6a9c52dc7f2 merged into trunk 45d01e77c26d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
lab_run ran npm ci at the root of mesh-sdk, mesh-tools and mesh-lab; mesh-tools keeps its package in
node-tools/ with no lock file at its root, so every run failed at its setup in nine seconds. A run now
installs (and, for the SDK and the base, builds) where a package-lock.json is: the root, or each
directory one level down; a package.json with no lock file fails the run naming it. It checks out
mesh-tools-go, which genesis builds since issue 616, and tells the beds where its checkouts are
(MESH_LAB_REPOS). lab_check asks the forge for every repository a run clones and applies the same install
rule to the ones it installs, so a run's setup failure is found before a run.
2026-10-13 11:55:01 +02:00
mesh-admin 45d01e77c2 Merge pull request 'fail2ban: know the own-addresses list in the form fail2ban prints it (hq issue 238)' (#267) from fix/238-fail2bans-own-file-form into main 2026-10-13 09:18:26 +00:00
jochen c78d0a0134 fail2ban: know the own-addresses list in the form fail2ban prints it (hq issue 238)
mesh/merge-gate head c78d0a01342c merged into trunk f35b06750452 — pass: builds fail2ban → ace, g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head c78d0a01342c merged into trunk f35b06750452 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
fail2ban reads the ignoreip entry file:/var/lib/... and prints it back as file://var/lib/...,
in get <jail> ignoreip and in its "Ignore ... by ..." log lines alike. The watch compared the
text, so on ace it said every jail lacked the list while each held it, failing fail2ban's health
and its walk; and it would never have counted a failure from the operator's own address. Compare
the path instead, with a test on fail2ban's real form.
2026-10-13 11:14:24 +02:00
11 changed files with 257 additions and 17 deletions
+3 -2
View File
@@ -19,8 +19,9 @@ runtime is missing or unreachable rather than an empty list.
- **An owner for a package both workstations already carry.** On both, `docker-compose` 5.5.0 is
installed explicitly by hand, as the plugin in `/usr/lib/docker/cli-plugins`. Assigning the module
changes nothing on disk; from then on the package is the mesh's, upgraded with the machine and
given back when the module goes.
changes nothing on disk; from then on the mesh says the package is needed, and it is upgraded with
the machine. Unassigning the module leaves the package installed: the node-engine never removes a
package a declaration stopped naming (novox/hq ADR 0303).
- **The projects become visible from the mesh** without a shell on the machine: which are running,
where their files are, their containers, logs and rendered configuration.
- **No account-level copy of the plugin** exists on either workstation (`~/.docker/cli-plugins` is
@@ -44,6 +44,20 @@ const (
IgnoredBy = "file:" + ListFile
)
// isOurList says whether an entry of a jail's ignore list, or the source the daemon's log names, is the list of
// the operator's own addresses. fail2ban keeps a file entry in its own form, not as written: it reads
// `file:/var/lib/…` and prints it back as `file://var/lib/…` (server/ipdns.py, FileIPAddrSet.__repr__, in 1.1),
// in `get <jail> ignoreip` and in its `Ignore … by …` lines alike. So the path is compared, with fail2ban's own
// reading of the slashes after `file:`, never the text (seen on ace on 2026-10-13: every jail held the list
// and the watch said none did).
func isOurList(word string) bool {
path, ok := strings.CutPrefix(strings.TrimSpace(word), "file:")
if !ok {
return false
}
return "/"+strings.TrimLeft(path, "/") == ListFile
}
// Own is the setting read: its entries, and each one that is not an address or a range, with why.
type Own struct {
Entries []netip.Prefix `json:"-"`
@@ -15,10 +15,15 @@ import (
"time"
)
// asPrinted is the list's entry as fail2ban 1.1 prints it back, in `get <jail> ignoreip` and in its log: it reads
// `file:/var/lib/…` and keeps it as `file://var/lib/…` (FileIPAddrSet.__repr__), as ace's daemon printed it on
// 2026-10-13.
const asPrinted = "file://var/lib/mesh-fail2ban/own-addresses"
// ignoring is a jail's ignore list as fail2ban-client 1.1 prints it, with the base the module writes.
const ignoring = "These IP addresses/networks are ignored:\n|- 127.0.0.0/8\n|- 10.10.0.0/24\n|- 10.0.0.0/8\n" +
"|- 172.16.0.0/12\n|- 192.168.0.0/16\n|- 169.254.0.0/16\n|- fc00::/7\n|- fe80::/10\n" +
"|- file:/var/lib/mesh-fail2ban/own-addresses\n`- ::1\n"
"|- " + asPrinted + "\n`- ::1\n"
// home is the operator's home uplink in these tests (RFC 5737).
const home = "203.0.113.5"
@@ -153,7 +158,7 @@ func appendLog(t *testing.T, root, text string) {
func daemon(missing bool) Runner {
gitea := ignoring
if missing {
gitea = strings.Replace(ignoring, "|- file:/var/lib/mesh-fail2ban/own-addresses\n", "", 1)
gitea = strings.Replace(ignoring, "|- "+asPrinted+"\n", "", 1)
}
return fake(map[string]string{"status": "Status\n|- Number of jail:\t2\n`- Jail list:\tgitea, recidive\n",
"get gitea ignoreip": gitea, "get recidive ignoreip": ignoring}, nil)
@@ -168,7 +173,7 @@ func TestAFailureIgnoredFromTheOperatorsOwnAddressIsSaidUntilADayAfterTheLast(t
clock := now
w := &Watcher{F: Fail2ban{Run: daemon(false)}, Root: root, Now: func() time.Time { return clock }}
appendLog(t, root, logAt(now.Add(-48*time.Hour), "gitea", home, IgnoredBy)+
appendLog(t, root, logAt(now.Add(-48*time.Hour), "gitea", home, asPrinted)+
logAt(now.Add(-time.Minute), "sshd", "192.168.1.1", "ip"))
s := w.Look(context.Background())
if err := WriteState(root, s); err != nil {
@@ -183,7 +188,7 @@ func TestAFailureIgnoredFromTheOperatorsOwnAddressIsSaidUntilADayAfterTheLast(t
var loop strings.Builder
for i := 0; i < 40; i++ {
loop.WriteString(logAt(now.Add(time.Duration(i)*15*time.Second), "gitea", home, IgnoredBy))
loop.WriteString(logAt(now.Add(time.Duration(i)*15*time.Second), "gitea", home, asPrinted))
}
appendLog(t, root, loop.String()[:loop.Len()-10]) // the daemon is still writing the last line
clock = now.Add(10 * time.Minute)
@@ -303,3 +308,21 @@ func TestTheManifestGivesEveryJailTheListAndTheWatchItsHealth(t *testing.T) {
t.Errorf("tools %v", m.Tools)
}
}
// The list is known by its path, in the form the module writes it and in the form fail2ban prints it back.
func TestTheListIsKnownInTheFormFail2banPrintsItBack(t *testing.T) {
for _, word := range []string{IgnoredBy, asPrinted, "file:///var/lib/mesh-fail2ban/own-addresses"} {
if !isOurList(word) {
t.Errorf("%q is the list and was not known", word)
}
}
for _, word := range []string{"ip", "/var/lib/mesh-fail2ban/own-addresses", "file://var/lib/mesh-fail2ban/other",
"file:/var/lib/mesh-fail2ban/own-addresses.new", "192.168.0.0/16"} {
if isOurList(word) {
t.Errorf("%q was taken for the list", word)
}
}
if !namesOurList(listed(ignoring)) {
t.Errorf("the jail's list as the daemon prints it was read as lacking the list")
}
}
+11 -2
View File
@@ -103,7 +103,7 @@ func (w *Watcher) Look(ctx context.Context) State {
s.Unread = append(s.Unread, fmt.Sprintf("the jail %s's ignore list cannot be read: %v", j, err))
continue
}
if !contains(listed(out), IgnoredBy) {
if !namesOurList(listed(out)) {
s.Missing = append(s.Missing, j)
}
}
@@ -115,6 +115,15 @@ func (w *Watcher) Look(ctx context.Context) State {
return s
}
func namesOurList(entries []string) bool {
for _, e := range entries {
if isOurList(e) {
return true
}
}
return false
}
func contains(list []string, want string) bool {
for _, l := range list {
if l == want {
@@ -162,7 +171,7 @@ func (w *Watcher) readLog(now time.Time) error {
// take keeps one line of the log when it is a failure ignored because of the operator's own addresses.
func (w *Watcher) take(line string, now time.Time) {
m := ignoredLine.FindStringSubmatch(line)
if m == nil || m[4] != IgnoredBy {
if m == nil || !isOurList(m[4]) {
return
}
at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local)
+1 -1
View File
@@ -2,7 +2,7 @@
The bars as a module (novox/hq ADR 0208, research 026/05).
- Installs `i3status-rust`. The update block's `checkupdates` comes from `pacman-contrib`, which the `pacman` module installs on every node; two modules declaring one package is refused at composition.
- Installs `i3status-rust`, and `pacman-contrib` for the update block's `checkupdates`. The `pacman` module declares `pacman-contrib` too; several modules may declare one package (novox/hq ADR 0303), so each declares what it needs.
Claims the mesh's `node-bar` seat (no verbs yet, ADR 0208 §2) and renders the blocks other modules
contribute to it (ADR 0255, below). Requires `x11-display` on its own
machine: its bars are i3bar's.
@@ -23,7 +23,8 @@ func TestItClaimsTheBarSeatAndRequiresTheXDisplay(t *testing.T) {
if !reflect.DeepEqual(m.Requires, []string{"x11-display"}) {
t.Fatalf("requires: %v", m.Requires)
}
if present, absent := m.packages(); !reflect.DeepEqual(present, []string{"i3status-rust"}) || absent != nil {
// pacman-contrib for the update block's checkupdates, declared here though the pacman module declares it too (ADR 0303).
if present, absent := m.packages(); !reflect.DeepEqual(present, []string{"i3status-rust", "pacman-contrib"}) || absent != nil {
t.Fatalf("packages: %v, absent %v", present, absent)
}
}
+5
View File
@@ -35,6 +35,11 @@
"type": "package",
"package": "i3status-rust"
},
{
"id": "checkupdates",
"type": "package",
"package": "pacman-contrib"
},
{
"id": "configuration-dir",
"type": "directory",
+3
View File
@@ -4,6 +4,9 @@
"description": "lab — the lab, as a module: runs beds against the forge's branches when the mesh asks (novox/hq ADR 0172).",
"type": "module",
"private": true,
"scripts": {
"test": "node --test --experimental-strip-types 'test/*.test.ts'"
},
"dependencies": { "@novox/mesh-sdk": "^0.1.0" },
"devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.6.0" }
}
+100
View File
@@ -0,0 +1,100 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { installPlan, installStep, NPM_REPOSITORIES, REPOSITORIES, script } from "../tools/runs.ts";
// Defends novox/hq issue 624: lab_run ran `npm ci` at the root of every checkout it installs, and
// mesh-tools keeps its package in node-tools/ with no lock file at its root, so every run failed at its
// setup, in 9 seconds, before anything was built.
function aCheckout(files: Record<string, string>): string {
const root = mkdtempSync(join(tmpdir(), "lab-checkout-"));
for (const [path, content] of Object.entries(files)) {
mkdirSync(join(root, path, ".."), { recursive: true });
writeFileSync(join(root, path), content);
}
return root;
}
test("a checkout with a lock file at its root is installed there", () => {
const root = aCheckout({ "package.json": "{}", "package-lock.json": "{}", "node-tools/package.json": "{}" });
assert.deepEqual(installPlan(root), { install: [root], unlocked: [] });
rmSync(root, { recursive: true, force: true });
});
test("a checkout whose package lives one directory down is installed there, as mesh-tools' node-tools", () => {
const root = aCheckout({ "node-tools/package.json": "{}", "node-tools/package-lock.json": "{}", "Dockerfile": "" });
assert.deepEqual(installPlan(root), { install: [join(root, "node-tools")], unlocked: [] });
rmSync(root, { recursive: true, force: true });
});
test("a package.json with no lock file is named, never installed", () => {
const root = aCheckout({ "package.json": "{}" });
assert.deepEqual(installPlan(root), { install: [], unlocked: [root] });
rmSync(root, { recursive: true, force: true });
});
// The shell a run executes follows the same rule: npm is asked where installPlan says, and a checkout
// with nothing installable fails the run loudly, naming it.
function runStep(root: string, build: boolean): { ok: boolean; said: string; asked: string[] } {
const bin = mkdtempSync(join(tmpdir(), "lab-npm-"));
const record = join(bin, "asked");
writeFileSync(join(bin, "npm"), `#!/bin/sh\necho "$(pwd) $*" >> ${JSON.stringify(record)}\n`);
chmodSync(join(bin, "npm"), 0o755);
const ran = spawnSync("bash", ["-c", `set -euo pipefail\n${installStep(root, build)}`], {
encoding: "utf8", env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
});
let asked: string[] = [];
try {
asked = readFileSync(record, "utf8").trim().split("\n").filter(Boolean);
} catch { /* nothing asked */ }
rmSync(bin, { recursive: true, force: true });
return { ok: ran.status === 0, said: `${ran.stdout}${ran.stderr}`, asked };
}
test("a run installs and builds mesh-tools in node-tools, where its lock file is", () => {
const root = aCheckout({ "node-tools/package.json": "{}", "node-tools/package-lock.json": "{}" });
const r = runStep(root, true);
assert.ok(r.ok, r.said);
assert.deepEqual(r.asked, [
`${join(root, "node-tools")} ci --no-audit --no-fund --loglevel=error`,
`${join(root, "node-tools")} run build --if-present`,
]);
rmSync(root, { recursive: true, force: true });
});
test("a run installs a checkout with its lock file at the root there, and builds only when asked", () => {
const root = aCheckout({ "package.json": "{}", "package-lock.json": "{}" });
const r = runStep(root, false);
assert.ok(r.ok, r.said);
assert.deepEqual(r.asked, [`${root} ci --no-audit --no-fund --loglevel=error`]);
rmSync(root, { recursive: true, force: true });
});
test("a run fails at its setup, naming the checkout, when nothing of it can be installed", () => {
const layouts: Record<string, string>[] = [{ "package.json": "{}" }, { "Dockerfile": "" }, { "sub/package.json": "{}" }];
for (const files of layouts) {
const root = aCheckout(files);
const r = runStep(root, false);
assert.equal(r.ok, false, `installed ${JSON.stringify(files)}`);
assert.match(r.said, /issue 624/);
assert.deepEqual(r.asked, []);
rmSync(root, { recursive: true, force: true });
}
});
// A run checks out mesh-tools-go too (novox/hq issue 616): genesis builds the Go toolchain from the
// repository beside mesh-tools, and a bed that raises genesis serves it. And it tells the beds where the
// checkouts are.
test("a run checks out mesh-tools-go and says where its checkouts are", () => {
assert.ok(REPOSITORIES.includes("mesh-tools-go"));
const s = script("/work", "run-x", "http://forge", ["test/integration/genesis-keeps-its-bus.test.ts"], {});
assert.match(s, /novox\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/);
assert.match(s, /export MESH_LAB_REPOS='\/work\/run-x'/);
assert.doesNotMatch(s, /cd '\/work\/run-x'\/\$repo && npm ci/);
for (const repo of NPM_REPOSITORIES) assert.ok(s.includes(`'/work/run-x/${repo}'/package-lock.json`), repo);
});
+39 -2
View File
@@ -4,7 +4,7 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { listRuns, readStatus, REPOSITORIES, running, start, stop, tail } from "./runs.js";
import { installPlan, listRuns, NPM_REPOSITORIES, readStatus, REPOSITORIES, running, start, stop, tail } from "./runs.js";
export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
const work = env.MESH_LAB_WORK ?? "/var/lib/mesh-lab-runs";
@@ -26,7 +26,12 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
if (clone.status !== 0) return { ok: false, output: clone.stderr };
spawnSync("npm", ["ci", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, encoding: "utf8" });
const check = spawnSync("node", ["--experimental-strip-types", "src/cli.ts", "check"], { cwd: dir, encoding: "utf8" });
return { ok: check.status === 0, output: `${check.stdout}${check.stderr}`.trim() };
// **And what a run would check out and install, before a run finds out** (novox/hq issue 624): every
// repository it clones answers on the forge, and every one it installs has a lock file where the run
// installs from.
const problems = setupProblems(forge, `${work}/check-setup`);
const output = [`${check.stdout}${check.stderr}`.trim(), ...problems].filter(Boolean).join("\n");
return { ok: check.status === 0 && problems.length === 0, output };
},
},
{
@@ -89,6 +94,38 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
];
}
/**
* What would stop a run at its setup, in sentences: a repository the run clones that the forge does not
* answer for, and a checkout it installs with nothing installable (installPlan). Empty when a run's setup
* would go through.
*/
export function setupProblems(forge: string, dir: string): string[] {
const problems: string[] = [];
spawnSync("rm", ["-rf", dir]);
for (const repo of REPOSITORIES) {
const url = `${forge}/novox/${repo}.git`;
const remote = spawnSync("git", ["ls-remote", "--heads", url, "main"], { encoding: "utf8" });
if (remote.status !== 0 || !remote.stdout.includes("refs/heads/main")) {
problems.push(`a run checks out ${repo}, and ${url} answers no main branch`);
continue;
}
if (!NPM_REPOSITORIES.includes(repo)) continue;
const at = `${dir}/${repo}`;
const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", url, at], { encoding: "utf8" });
if (clone.status !== 0) {
problems.push(`${repo} could not be cloned from ${url}: ${clone.stderr.trim()}`);
continue;
}
const plan = installPlan(at);
for (const d of plan.unlocked) problems.push(`${repo}: ${d.slice(at.length) || "/"} has a package.json and no package-lock.json, so a run's npm ci cannot install it`);
if (plan.install.length === 0 && plan.unlocked.length === 0) {
problems.push(`${repo}: no package-lock.json at its root or one directory down, so a run installs nothing of it`);
}
}
spawnSync("rm", ["-rf", dir]);
return problems;
}
/** One word from an env-file (`KEY=value` lines), or "" when the file or the word is absent. */
export function wordIn(file: string | undefined, word: string): string {
if (!file) return "";
+52 -5
View File
@@ -10,8 +10,55 @@ import { spawn } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
/** The repositories a lab run checks out, side by side, as the lab expects its siblings. */
export const REPOSITORIES = ["mesh-lab", "mesh-controller", "mesh-host", "mesh-catalog", "mesh-tools", "mesh-sdk"];
/**
* The repositories a lab run checks out, side by side, as the lab expects its siblings. mesh-tools-go is
* one since genesis builds the Go toolchain at its base step, from the repository beside mesh-tools
* (novox/hq issue 616): a bed that raises genesis serves it from its forge.
*/
export const REPOSITORIES = ["mesh-lab", "mesh-controller", "mesh-host", "mesh-catalog", "mesh-tools", "mesh-tools-go", "mesh-sdk"];
/** The checkouts whose JavaScript dependencies a run installs: the lab, the SDK and the base. */
export const NPM_REPOSITORIES = ["mesh-sdk", "mesh-tools", "mesh-lab"];
/**
* Where a checkout's JavaScript dependencies are installed (novox/hq issue 624): its root when the root
* holds a lock file, otherwise each directory one level down that holds one. `npm ci` installs only from a
* lock file, and a repository that moved its package into a directory of its own (mesh-tools' node-tools)
* has none at its root; the run asked for one there and failed before it built anything. A package.json
* with no lock file beside it cannot be installed reproducibly, and is named rather than installed.
*/
export function installPlan(root: string): { install: string[]; unlocked: string[] } {
const has = (dir: string, file: string) => existsSync(join(dir, file));
if (has(root, "package-lock.json")) return { install: [root], unlocked: [] };
const install: string[] = [];
const unlocked: string[] = has(root, "package.json") ? [root] : [];
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
const dir = join(root, entry.name);
if (has(dir, "package-lock.json")) install.push(dir);
else if (has(dir, "package.json")) unlocked.push(dir);
}
return { install: install.sort(), unlocked: unlocked.sort() };
}
/**
* installStep is the shell a run installs one checkout with: the same rule as installPlan, so what
* lab_check says a run would install is what it installs. Building runs where the package says how.
*/
export function installStep(root: string, build: boolean): string {
const q = shellQuote(root);
const install = `(cd "$d" && npm ci --no-audit --no-fund --loglevel=error${build ? " && npm run build --if-present" : ""})`;
return `if [ -f ${q}/package-lock.json ]; then d=${q}; ${install}; else
[ ! -f ${q}/package.json ] || { echo "${root}: a package.json and no package-lock.json, so npm ci cannot install it (novox/hq issue 624)" >&2; exit 1; }
found=0
for d in ${q}/*/; do
d="\${d%/}"; [ "$(basename "$d")" = node_modules ] && continue
if [ -f "$d/package-lock.json" ]; then found=1; ${install}
elif [ -f "$d/package.json" ]; then echo "$d: a package.json and no package-lock.json, so npm ci cannot install it (novox/hq issue 624)" >&2; exit 1; fi
done
[ "$found" = 1 ] || { echo "${root}: no package-lock.json at its root or one directory down, so nothing of it can be installed (novox/hq issue 624)" >&2; exit 1; }
fi`;
}
export interface RunStatus {
id: string;
@@ -104,9 +151,7 @@ ${setState("building")}
# resolves it; nothing else is asked of it.
printf '%s\n' ${shellQuote(`@novox:registry=${forge}/api/packages/novox/npm/`)} > ${shellQuote(join(dir, ".npmrc"))}
export NPM_CONFIG_USERCONFIG=${shellQuote(join(dir, ".npmrc"))}
for repo in mesh-sdk mesh-tools mesh-lab; do (cd ${shellQuote(dir)}/$repo && npm ci --no-audit --no-fund --loglevel=error); done
(cd ${shellQuote(dir)}/mesh-sdk && npm run build --if-present)
(cd ${shellQuote(dir)}/mesh-tools && npm run build --if-present)
${NPM_REPOSITORIES.map((repo) => installStep(join(dir, repo), repo !== "mesh-lab")).join("\n")}
mkdir -p ${shellQuote(bin)}
for p in postgres-provisioner objectstore-provisioner route-proxy; do
(cd ${shellQuote(dir)}/mesh-controller && CGO_ENABLED=0 go build -o ${shellQuote(bin)}/$p ./examples/$p)
@@ -117,6 +162,8 @@ export MESH_LAB_MODULES=${shellQuote(join(dir, "mesh-controller", "examples", "m
export MESH_LAB_BUILDER=${shellQuote(join(dir, "mesh-controller", "build", "mesh-builder"))}
export MESH_LAB_BOOTSTRAP_BINARY=${shellQuote(join(dir, "mesh-host", "mesh-bootstrap"))}
export MESH_LAB_CATALOG=${shellQuote(join(dir, "mesh-catalog", "modules"))}
# Where a bed that serves repositories from its own forge finds them: this run's checkouts, side by side.
export MESH_LAB_REPOS=${shellQuote(dir)}
export MESH_LAB_PROVISIONER=${shellQuote(join(bin, "postgres-provisioner"))}
export MESH_LAB_OBJECTSTORE_PROVISIONER=${shellQuote(join(bin, "objectstore-provisioner"))}
export MESH_LAB_ROUTE_PROXY=${shellQuote(join(bin, "route-proxy"))}