diff --git a/checks/health/go.mod b/checks/health/go.mod new file mode 100644 index 00000000..ca75b27a --- /dev/null +++ b/checks/health/go.mod @@ -0,0 +1,5 @@ +module checks/health + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022190740-104533e8c0c9 diff --git a/checks/health/go.sum b/checks/health/go.sum new file mode 100644 index 00000000..fc86453e --- /dev/null +++ b/checks/health/go.sum @@ -0,0 +1,4 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac h1:jnZ/n+fFMo7VQPNNXK1gjStHIJPw3SPbzPxsf/PpZqw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022190740-104533e8c0c9 h1:Ufmd2cYEXICQ817IGflccP9bexABE4WhIO49HPbaoSo= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022190740-104533e8c0c9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/checks/health/health.go b/checks/health/health.go new file mode 100644 index 00000000..0797da65 --- /dev/null +++ b/checks/health/health.go @@ -0,0 +1,70 @@ +// Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule +// 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of +// merge-check.sh that read it out of the controller's `module check`. Every manifest is read through mesh-sdk's catalogue +// package, the one the controller counts with. +package health + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Count is the long-running resources without `health` of every module under root/modules. +func Count(root string) (int, error) { + manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) + if err != nil { + return 0, err + } + sort.Strings(manifests) + if len(manifests) == 0 { + return 0, fmt.Errorf("no manifest under %s: the count would be zero for any catalogue", filepath.Join(root, "modules")) + } + n := 0 + for _, path := range manifests { + m, err := catalogue.ReadTreeManifest(root, filepath.Dir(path)) + if err != nil { + return 0, fmt.Errorf("%s: %w", path, err) + } + n += len(catalogue.Undeclared(m)) + } + return n, nil +} + +// Kept is the number the file keeps: the first number of its first line. +func Kept(path string) (int, error) { + raw, err := os.ReadFile(path) + if err != nil { + return 0, err + } + first, _, _ := strings.Cut(string(raw), "\n") + digits := "" + for _, r := range strings.TrimSpace(first) { + if r < '0' || r > '9' { + break + } + digits += string(r) + } + n, err := strconv.Atoi(digits) + if err != nil { + return 0, fmt.Errorf("%s does not begin with the number it keeps: %q", path, first) + } + return n, nil +} + +// Judge says what is wrong with a count against the kept number; empty when they agree. It only goes down: a rise is refused, +// and a fall asks the new number written in health-undeclared, so the count cannot rise again. +func Judge(kept, counted int) string { + switch { + case counted > kept: + return fmt.Sprintf("the long-running resources without health rose from %d to %d: declare how each new one is ready (ADR 0240 rule 8)", kept, counted) + case counted < kept: + return fmt.Sprintf("the long-running resources without health fell from %d to %d: write %d in health-undeclared, so the count cannot rise again", kept, counted, counted) + } + return "" +} diff --git a/checks/health/health_test.go b/checks/health/health_test.go new file mode 100644 index 00000000..c0b445b5 --- /dev/null +++ b/checks/health/health_test.go @@ -0,0 +1,88 @@ +package health + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func aCatalogue(t *testing.T, manifests map[string]string) string { + t.Helper() + root := t.TempDir() + for module, body := range manifests { + dir := filepath.Join(root, "modules", module) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + return root +} + +// A service that stays up and does not say how it is ready is counted; one that says, and a file, are not. +func TestOnlyALongRunningResourceWithoutHealthIsCounted(t *testing.T) { + root := aCatalogue(t, map[string]string{ + "quiet": `{"module":"quiet","resources":[{"id":"svc","type":"service","unit":"quiet.service","state":"running"}]}`, + "says": `{"module":"says","resources":[{"id":"svc","type":"service","unit":"says.service","state":"running",` + + `"health":{"kind":"unit"}}]}`, + "file": `{"module":"file","resources":[{"id":"cfg","type":"file","path":"/etc/file.conf","content":"x"}]}`, + }) + counted, err := Count(root) + if err != nil { + t.Fatal(err) + } + if counted != 1 { + t.Fatalf("counted %d, wanted the one quiet service", counted) + } +} + +func TestTheCountMayOnlyGoDown(t *testing.T) { + for _, c := range []struct { + kept, counted int + says string + }{ + {58, 58, ""}, + {58, 59, "rose from 58 to 59"}, + {58, 57, "fell from 58 to 57"}, + } { + got := Judge(c.kept, c.counted) + if (c.says == "") != (got == "") || !strings.Contains(got, c.says) { + t.Errorf("kept %d counted %d: %q, wanted %q", c.kept, c.counted, got, c.says) + } + } +} + +func TestTheKeptCountIsTheFirstNumberOfTheFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "health-undeclared") + if err := os.WriteFile(path, []byte("58\nThe long-running resources ...\n"), 0o644); err != nil { + t.Fatal(err) + } + if n, err := Kept(path); err != nil || n != 58 { + t.Fatalf("%d %v", n, err) + } + if err := os.WriteFile(path, []byte("many\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Kept(path); err == nil { + t.Fatal("a file with no number gave a count") + } +} + +// The script's `module check` run over the real tree (novox/hq ADR 0240 rule 8, ADR 0529 clause 2 and 4): the long-running +// resources that do not say how they are ready, counted as the controller counts them, are the number health-undeclared keeps. +func TestTheCatalogueSaysHowItsLongRunningResourcesAreReady(t *testing.T) { + kept, err := Kept(filepath.Join("..", "..", "health-undeclared")) + if err != nil { + t.Fatal(err) + } + counted, err := Count(filepath.Join("..", "..")) + if err != nil { + t.Fatal(err) + } + if say := Judge(kept, counted); say != "" { + t.Fatal(say) + } +} diff --git a/health-undeclared b/health-undeclared index 9ce896e4..ad3ead71 100644 --- a/health-undeclared +++ b/health-undeclared @@ -1,5 +1,5 @@ 58 The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240 -rule 8), as `module check` counts them. It may only go down: merge-check.sh fails a change that raises it, and +rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running resource without `health` is refused. diff --git a/merge-check-typescript.sh b/merge-check-typescript.sh deleted file mode 100644 index b32bb08f..00000000 --- a/merge-check-typescript.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/bin/sh -# The catalogue's TypeScript part of its own check (novox/hq issue 302's `mesh-check-also`), declared in -# merge-check.sh's header and run by the build seat in the TypeScript toolchain, after the Go part. The media -# catalogue's twin is issue 311. Before this, a TypeScript module's change ran nothing of its own before it -# merged: the Go part said "NOT TESTED HERE", and a type error in the forge module would first have shown in -# its walk. -# -# For every touched module that has a package.json, in a copy of it outside the clone: -# -# 1. its dependencies are the toolchain's own (/app/node_modules: the mesh's SDK, Node's types and what the -# runtime carries), copied beside it, never fetched: the check holds no credential for the package -# registry. A module that depends on a package the toolchain does not hold **fails the check**, by name: -# it could not be type-checked or tested, and what could not run is a failure, never a note (novox/hq -# issue 385); -# 2. every source that is not a test is type-checked with the toolchain's compiler and the flags a build -# compiles with — the same check the build makes, before the merge rather than in the walk; -# 3. its tests (test/*.test.ts) run under Node's own runner with types stripped, as its own `npm test` does -# after its build: the sources are compiled to dist/ and beside themselves first, so a test that imports -# the compiled module, or a source that imports its sibling by its .js name, finds it. -# -# Said, never passed silently: a module with no tests is said NOT TESTED on every run that touches it — -# nothing of it could fail. A module whose tests exist and could not run fails (novox/hq issue 385). -# -# By hand: MESH_CHECK_MODULES=gitea sh merge-check-typescript.sh, in the toolchain image (it reads -# /app/node_modules), or with TOOLCHAIN_MODULES pointing at a node_modules that holds the same. -# -# **In an agent's session, outside the toolchain image** (novox/hq issue 742), the same node_modules is -# assembled first, in a throwaway directory under the check's own workspace, the way the image's `deps` stage -# assembles it: the toolchain's own package.json and lock (mesh-tools' node-tools/, read from the forge this -# clone came from, so the list is never copied here) installed with `npm ci`, then the SDK the mesh last -# published (`@novox/mesh-sdk@latest`), the @novox scope from the forge's package registry, read without a -# credential. Node and npm are the dev-tools module's; nothing is installed on the machine, no npm -# configuration or cache of the account's is read or written. Only when neither TOOLCHAIN_MODULES nor -# /app/node_modules is there: the forge's check, in the image, fetches nothing, as before. The forge is -# MESH_FORGE (default: the origin remote's), the owner MESH_FORGE_OWNER (default: the origin's owner). -set -eu - -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT - -deps="${TOOLCHAIN_MODULES:-/app/node_modules}" -if [ -z "${TOOLCHAIN_MODULES:-}" ] && [ ! -f "$deps/typescript/bin/tsc" ]; then - origin=$(git remote get-url origin 2>/dev/null || true) - forge="${MESH_FORGE:-$(printf '%s\n' "$origin" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')}" - owner="${MESH_FORGE_OWNER:-$(printf '%s\n' "$origin" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')}" - if [ -z "$forge" ] || [ -z "$owner" ]; then - echo "NOT CHECKED: no TypeScript toolchain at $deps, and no forge to assemble one from (origin is '$origin'; set MESH_FORGE and MESH_FORGE_OWNER, or TOOLCHAIN_MODULES)" - exit 1 - fi - echo "no TypeScript toolchain at $deps: assembling one from $forge/$owner/mesh-tools node-tools/ and $owner's package registry (novox/hq issue 742)" - tc="$work/toolchain" - mkdir -p "$tc" - printf '@%s:registry=%s/api/packages/%s/npm/\n' "$owner" "$forge" "$owner" >"$work/npmrc" - # Read with Node's own fetch (dev-tools declares Node, not curl); a status that is not 2xx fails. - if ! { node -e ' - const [base, dir] = process.argv.slice(1); - Promise.all(["package.json", "package-lock.json"].map(async (f) => { - const r = await fetch(base + f); - if (!r.ok) throw new Error(base + f + ": " + r.status); - require("fs").writeFileSync(dir + "/" + f, await r.text()); - })).catch((e) => { console.error(e.message); process.exit(1); });' \ - "$forge/$owner/mesh-tools/raw/branch/main/node-tools/" "$tc" && - (cd "$tc" && export npm_config_userconfig="$work/npmrc" npm_config_cache="$work/npm-cache" && - npm ci --ignore-scripts --no-audit --no-fund --loglevel=error && - npm install --ignore-scripts --no-audit --no-fund --no-save --loglevel=error "@$owner/mesh-sdk@latest"); }; then - echo "NOT CHECKED: the TypeScript toolchain could not be assembled from $forge (novox/hq issue 742)" - exit 1 - fi - deps="$tc/node_modules" -fi -tsc="$deps/typescript/bin/tsc" -if [ ! -f "$tsc" ]; then - echo "NOT CHECKED: no TypeScript compiler at $tsc — this part runs in the TypeScript toolchain" - exit 1 -fi - -# The modules the change reaches: the planner's answer (hq ADR 0238), and every module a changed file is in — -# a change to a module's tests alone moves no build (hq ADR 0267), and its tests must still run. -touched=$({ - printf '%s\n' "${MESH_CHECK_MODULES:-}" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d' - printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p' -} | sed '/^$/d' | sort -u) - -failed="" -for m in $touched; do - d="modules/$m" - [ -f "$d/package.json" ] || continue - copy="$work/$m" - mkdir -p "$copy" - (cd "$d" && tar --exclude=./node_modules --exclude=./dist -cf - .) | (cd "$copy" && tar -xf -) - # What it depends on, against what the toolchain holds. - missing=$(node -e ' - const p = require(process.argv[1]); const have = require("fs").readdirSync(process.argv[2]); - const scoped = (n) => n.startsWith("@") ? n.split("/")[0] : n; - const names = Object.keys({ ...(p.dependencies || {}), ...(p.devDependencies || {}) }); - const held = (n) => have.includes(n) || (n.startsWith("@") && have.includes(scoped(n)) && - require("fs").existsSync(process.argv[2] + "/" + n)); - console.log(names.filter((n) => n !== "typescript" && !held(n)).join(" "));' "$copy/package.json" "$deps") - if [ -n "$missing" ]; then - echo "FAILED: $d could not be checked — it depends on $missing, which the toolchain does not hold and the check cannot fetch (novox/hq issue 385)" - failed="$failed $m" - continue - fi - mkdir -p "$copy/node_modules" - for p in "$deps"/*; do - case "$(basename "$p")" in typescript | esbuild | @esbuild) continue ;; esac - cp -r "$p" "$copy/node_modules/" - done - sources=$(cd "$copy" && find . -name '*.ts' ! -name '*.d.ts' ! -path './test/*' ! -path './node_modules/*' ! -path './dist/*' | sort) - if [ -n "$sources" ]; then - echo "type-checking $d (TypeScript)" - # shellcheck disable=SC2086 # the module's sources, none with a space in its name - if ! (cd "$copy" && node "$tsc" --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 \ - --types node $sources); then - failed="$failed $m" - continue - fi - else - echo "$d has no TypeScript sources of its own to type-check: its tests read its files" - fi - if ! ls "$copy"/test/*.test.ts >/dev/null 2>&1; then - echo "NOT TESTED: $d is TypeScript and has no tests of its own" - continue - fi - if [ -n "$sources" ]; then - # shellcheck disable=SC2086 - (cd "$copy" && node "$tsc" --module NodeNext --moduleResolution NodeNext --target ES2022 --types node \ - --rootDir . --outDir dist $sources && node "$tsc" --module NodeNext --moduleResolution NodeNext \ - --target ES2022 --types node --rootDir . --outDir . $sources) - fi - echo "testing $d (TypeScript)" - if ! (cd "$copy" && node --test --test-reporter=spec --experimental-strip-types \ - --no-warnings=ExperimentalWarning test/*.test.ts); then - failed="$failed $m" - fi -done -if [ -n "$failed" ]; then - echo "TypeScript modules that failed:$failed" - exit 1 -fi diff --git a/merge-check.sh b/merge-check.sh deleted file mode 100755 index 834220ae..00000000 --- a/merge-check.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/sh -# mesh-check-toolchain: go -# mesh-check-also: typescript merge-check-typescript.sh -# -# The catalogue's own check (novox/hq ADR 0237 as amended): the second layer of a pull request's merge -# check, `mesh/repo-check`, run by the build seat in the mesh's Go toolchain. -# -# The gate — the manifests the change touches through the running controller's module check, every machine -# of the facts snapshot composed with them and validated by the node-engine's own validator, the replays -# against this tree — is the build seat's first layer (`mesh/merge-gate`), run because the mesh's module -# graph builds these modules from here. It is not repeated here. This is the repository's own: -# -# 1. every manifest through the controller's module check, so one module's change cannot leave another -# it shares a rule with refused (MESH_GATE is the controller the mesh runs) — and the count of -# long-running resources without `health` held to the number in health-undeclared, which only goes down; -# 2. the Go tests of every module the change touches that has them, under the race detector when the -# toolchain has a C compiler. A requirement of the mesh's own forge the module cache does not hold (a -# pinned SDK commit) is taken from its repository's clone beside the check (checks/deps); a module whose -# dependencies still cannot be had **fails the check**, by name and why — its tests could not run, and a -# test that could not run is a failure, never a note (novox/hq issue 385). The integration tests (`//go:build -# integration`) run here, with -tags integration, and only here. A module written in -# TypeScript is type-checked and tested by -# merge-check-typescript.sh, declared above, in the TypeScript toolchain (novox/hq issues 302, 311); -# 3. the words (novox/hq ADR 0244): no word the glossary retired for the tools, as copied in -# retired-words, in any text a module's tools can show an agent — every string in its own code that is -# not a test, and every description in its manifest. Run over every module, not only the touched ones, -# so a word newly retired is found everywhere it stands. The check's own test runs first. -# 4. the paths (novox/hq ADR 0260): no module names a path under /run, /var/lib, /usr/local/lib or /etc -# that another module's manifest places — modules talk over the bus, not through each other's files. -# Over every module; checks/paths/shared and checks/paths/known narrow it, and known only shrinks. -# 5. the agent's home (novox/hq ADR 0266): no module unpacks an archive, or writes into a file, under -# ${machine:agent-home} — the node-engine does both after checking the path, and the agent account owns -# that home and could swap a link in between. -# 6. the secrets (novox/hq issue 342): no mergeable file fills a key with a vault secret that a setting could -# replace — the key is `protected`, or left out of the file's `takes-settings`. checks/secrets/known names -# the nested ones the controller cannot protect alone yet, and only shrinks. -# 7. the list of predefined keys (novox/hq ADR 0348 clause 7): a change to predefined.json removes no key a -# module still names, judged against the trunk's copy at the merge base. Whether a removed key still holds a -# value at the mesh or a node is mesh-config's to refuse; this check reads no store. The list itself and -# every manifest against it are judged by the controller's module check in step 1. -# 8. the config form (novox/hq ADR 0348 clauses 1 and 11): no module says `settings`, `takes-settings` or -# `${setting:}`, in its manifest or a template it names, or has a mergeable file naming no keys (which -# takes its whole layer), unless checks/config/unmoved lists it as not yet moved; that list only shrinks, -# judged against the trunk's copy at the merge base. -# 9. the file text (novox/hq ADR 0349 §2): no module keeps a file's text inline in its manifest (`content`) -# unless checks/templates/inline lists it as not yet moved; each names its template in files/. That list -# only shrinks, judged against the trunk's copy at the merge base. -# 10. the mail-store seat (novox/hq ADR 0378 §3, issue 800): no manifest but modules/mesh-mail claims it. -# 11. owned paths (novox/hq ADR 0422 §8, issue 1024; it holds ~/.ssh for ssh-client as ADR 0415 §6 asked): -# every directory and archive says `unknown` (move-out, module or program) and a valid `knows`; `move-out` -# only under a home; no module declares a path inside a directory of another module; and none is, lies in or -# holds a directory a system package owns under /var/lib (checks/owned/system, issue 1841). -# checks/sshdir (ADR 0415 §6) stays while main runs it; it goes under issue 1035 once main runs checks/owned. -# 12. the image builder (novox/hq ADR 0487 §3, issue 1704): only modules/buildah provides rootless-image-build. -# 13. the image's place (novox/hq ADR 0487 §1, issue 1707): every image builds from a Dockerfile in its own module's directory. -set -eu - -(cd checks/words && go test -count=1 . && go run . ../..) -# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in a covered module; the names are in -# `particulars`, the covered modules in checks/particulars/covered. -(cd checks/particulars && go test -count=1 . && go run . ../..) -# The toolchains (novox/hq ADR 0477 clause 4): the package go only in modules/go, nodejs and npm only in modules/nodejs. -(cd checks/toolchains && go test -count=1 . && go run . ../..) -# The image builder (novox/hq ADR 0487 §3): only modules/buildah provides rootless-image-build, docker never. -(cd checks/imagebuild && go test -count=1 . && go run . ../..) -# The image's place (novox/hq ADR 0487 §1): every image builds from a Dockerfile in its own module's directory. -(cd checks/imageplace && go test -count=1 . && go run . ../..) -# The Node lock files (novox/hq ADR 0469 §5, issue 1868): a TypeScript module that names packages beyond the SDK commits its lock. -(cd checks/locks && go test -count=1 . && go run . ../..) -# The agent's home (novox/hq ADR 0266): no archive, and no file written into, under it. -(cd checks/agenthome && go test -count=1 . && go run . ../..) -# The mail-store seat (novox/hq ADR 0378 §3): only mesh-mail may claim it. -(cd checks/mailstore && go test -count=1 . && go run . ../..) -# ~/.ssh (novox/hq ADR 0415 §6): only ssh-client may name a path under it. Main's merge check runs this line, so -# checks/sshdir stays until main runs checks/owned; it goes under issue 1035. -(cd checks/sshdir && go test -count=1 . && go run . ../..) -# Owned paths (novox/hq ADR 0422 §8): unknown files named per directory; no path inside another module's directory; -# none in a system package's directory under /var/lib (issue 1841), the known exceptions only shrinking: judged -# against the trunk's copy of checks/owned/system-known at the merge base; a trunk without one is the list's first copy. -owned_base="" -if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then - if git cat-file -e "$merge_base:checks/owned/system-known" 2>/dev/null; then - owned_base=$(mktemp) - git show "$merge_base:checks/owned/system-known" >"$owned_base" - fi -else - echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/owned/system-known is held to the list it was first written with" -fi -(cd checks/owned && go test -count=1 . && if [ -n "$owned_base" ]; then go run . -base "$owned_base" ../..; else go run . ../..; fi) -# The secrets (novox/hq issue 342): a password is not a setting, so no setting can replace the vault's. The -# known list only shrinks: judged against the trunk's copy at the merge base, a line it does not hold is refused. -secrets_base="" -secrets_ref="origin/${MESH_CHECK_BASE:-main}" -if merge_base=$(git merge-base HEAD "$secrets_ref" 2>/dev/null); then - if git cat-file -e "$merge_base:checks/secrets/known" 2>/dev/null; then - secrets_base=$(mktemp) - git show "$merge_base:checks/secrets/known" >"$secrets_base" - fi -else - echo "NOT COMPARED WITH THE TRUNK: no merge base with $secrets_ref; checks/secrets/known is held to the list it was first written with" -fi -(cd checks/secrets && go test -count=1 . && if [ -n "$secrets_base" ]; then go run . -base "$secrets_base" ../..; else go run . ../..; fi) -# The list of predefined keys (novox/hq ADR 0348 clause 7): no key removed that a module still names. The trunk's -# list at the merge base is what it is judged against; a trunk without the file had none. -predefined_base="" -if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then - predefined_base=$(mktemp) - if ! git show "$merge_base:predefined.json" >"$predefined_base" 2>/dev/null; then - echo '{}' >"$predefined_base" - fi -else - echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; no removal from predefined.json can be seen" -fi -(cd checks/predefined && go test -count=1 . && if [ -n "$predefined_base" ]; then go run . -base "$predefined_base" ../..; else go run . ../..; fi) -# The config form (novox/hq ADR 0348 §1, §11): a moved module says no old name, and the list of modules not yet moved -# only shrinks, judged against the trunk's copy at the merge base; a trunk without one is the list's first copy. -unmoved_base="" -if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then - if git cat-file -e "$merge_base:checks/config/unmoved" 2>/dev/null; then - unmoved_base=$(mktemp) - git show "$merge_base:checks/config/unmoved" >"$unmoved_base" - fi -else - echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/config/unmoved is held to the list it was first written with" -fi -(cd checks/config && go test -count=1 . && if [ -n "$unmoved_base" ]; then go run . -base "$unmoved_base" ../..; else go run . ../..; fi) -# The file text (novox/hq ADR 0349 §2): a file's text is a template in files/, outside the modules not yet moved; the -# list of those only shrinks, judged against the trunk's copy at the merge base, a trunk without one being its first. -inline_base="" -if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then - if git cat-file -e "$merge_base:checks/templates/inline" 2>/dev/null; then - inline_base=$(mktemp) - git show "$merge_base:checks/templates/inline" >"$inline_base" - fi -else - echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/templates/inline is held to the list it was first written with" -fi -(cd checks/templates && go test -count=1 . && if [ -n "$inline_base" ]; then go run . -base "$inline_base" ../..; else go run . ../..; fi) -# The lists that narrow the paths check may not grow unseen: the base branch's copies are what they are -# judged against (MESH_CHECK_BASE, main when unset), read from the clone the check runs in. -base_lists="" -base_ref="origin/${MESH_CHECK_BASE:-main}" -if git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null 2>&1 && - git cat-file -e "$base_ref:checks/paths/known" 2>/dev/null && git cat-file -e "$base_ref:checks/paths/shared" 2>/dev/null; then - base_lists=$(mktemp -d) - git show "$base_ref:checks/paths/known" >"$base_lists/known" - git show "$base_ref:checks/paths/shared" >"$base_lists/shared" -fi -(cd checks/paths && go test -count=1 . && if [ -n "$base_lists" ]; then go run . -base "$base_lists" ../..; else go run . ../..; fi) - -if [ -n "${MESH_GATE:-}" ]; then - checked=$("$MESH_GATE" module check modules/*/module.json) || { printf '%s\n' "$checked"; exit 1; } - # **The count only goes down** (novox/hq ADR 0240 rule 8): the long-running resources that do not say how - # they are ready, as the controller counts them, against the number kept in health-undeclared. A change - # that raises it fails; one that lowers it writes the new number there, so it can never rise again. - kept=$(sed -n 's/^\([0-9][0-9]*\).*/\1/p' health-undeclared | head -n 1) - counted=$(printf '%s\n' "$checked" | sed -n 's/^long-running resources without health: \([0-9][0-9]*\)$/\1/p') - if [ -z "$counted" ]; then - echo "NOT COUNTED: the controller the mesh runs is older than the count of resources without health (ADR 0240 Phase B)" - elif [ "$counted" -gt "$kept" ]; then - echo "the long-running resources without health rose from $kept to $counted: declare how each new one is ready (ADR 0240 rule 8)" - exit 1 - elif [ "$counted" -lt "$kept" ]; then - echo "the long-running resources without health fell from $kept to $counted: write $counted in health-undeclared, so the count cannot rise again" - exit 1 - else - echo "long-running resources without health: $counted, as kept" - fi -else - echo "NOT CHECKED: no controller was built beside this check, so the manifests were not read by one" -fi - -race="" -if command -v gcc >/dev/null 2>&1; then race="-race"; else echo "NOT RACE-CHECKED: the toolchain holds no C compiler"; fi - -# The modules the change reaches are the controller's planner's answer (MESH_CHECK_MODULES, hq ADR 0238): -# a module by its name, a new one by its directory. By hand, without it, the directories the changed files -# are in. -if [ -n "${MESH_CHECK_MODULES:-}" ]; then - touched=$(printf '%s\n' "$MESH_CHECK_MODULES" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d' | sort -u) -else - touched=$(printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p' | sort -u) -fi -# A module's dependencies: fetched, or a forge requirement taken from its clone beside the check -# (MESH_CHECK_BESIDE); its tests run against them (checks/deps, novox/hq issue 385). The helper's own test -# runs first. -(cd checks/deps && go test -count=1 .) -deps=$(mktemp -d) -trap 'rm -rf "$deps"' EXIT -(cd checks/deps && go build -o "$deps/deps" .) -untestable="" -for m in $touched; do - [ -d "modules/$m" ] || continue - if [ ! -f "modules/$m/go.mod" ]; then - [ -f "modules/$m/package.json" ] && echo "modules/$m is TypeScript: merge-check-typescript.sh type-checks and tests it" - continue - fi - if ! modfile=$(GOPRIVATE=git.novox.be "$deps/deps" -beside "${MESH_CHECK_BESIDE:-}" -work "$deps" "modules/$m"); then - echo "FAILED: modules/$m could not be tested — $modfile" - untestable="$untestable modules/$m" - continue - fi - [ -n "$modfile" ] && echo "modules/$m takes its forge dependencies from the clones beside the check" - echo "testing modules/$m" - unformatted=$(cd "modules/$m" && gofmt -l .) - if [ -n "$unformatted" ]; then - echo "modules/$m is not gofmt'd: $unformatted" - exit 1 - fi - cgo=0 - [ -n "$race" ] && cgo=1 - # The integration tests (a file whose first line is `//go:build integration`: a test that starts a bus, a store or - # another process) run here and nowhere else: the unit suite stays under a second (novox/hq playbook 07). - (cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet -tags integration ${modfile:+"-modfile=$modfile"} ./... && - CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} $race -count=1 ./...) - # The docker tests of the bus's snapshot program and entrypoint (novox/hq ADR 0480 clause 6) start throwaway nats - # containers, and the Go toolchain container holds no docker, so above they skip with a "NOT RUN" line. Where the - # build seat says docker exists (MESH_CHECK_DOCKER=1) they run here by name, and a missing docker fails the check - # (MESH_REQUIRE_DOCKER=1 makes the tests fail instead of skip). Without it the step says it did not run. - if [ "$m" = nats ]; then - if [ "${MESH_CHECK_DOCKER:-}" = 1 ]; then - (cd modules/nats && MESH_REQUIRE_DOCKER=1 CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} -count=1 \ - -run 'TestTheDeclaredDumpRunsInTheBussImage|TestTheEntrypointReloadsTheServerWithoutDroppingAConnection|TestTheStoreSurvivesTheNewDeclarationTheOldAndTheNewAgain|TestASnapshotOfALiveBusRestoresToTheSameContent|TestTheSnapshotUserCannotChangeTheBus' \ - ./cmd/mesh-nats-snapshot) || { echo "the bus's docker tests failed"; exit 1; } - else - echo "NOT RUN: the bus's five docker tests (modules/nats/cmd/mesh-nats-snapshot) skipped: this check has no docker (MESH_CHECK_DOCKER is not 1)" - fi - fi -done -if [ -n "$untestable" ]; then - echo "the check fails: the tests of$untestable could not run (novox/hq issue 385)" - exit 1 -fi