Check that only the go and nodejs modules declare the toolchain packages (issue 1573)

This commit is contained in:
2026-10-21 09:50:14 +00:00
parent 7753c27042
commit c4cd7a1b16
5 changed files with 175 additions and 4 deletions
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/toolchains
go 1.22
+104
View File
@@ -0,0 +1,104 @@
// Command toolchains refuses any manifest but the owner's declaring a language's toolchain package (novox/hq
// ADR 0477 clause 4, issue 1573): the package go only in modules/go, and nodejs and npm only in modules/nodejs.
// Everything that compiles or runs that language requires the owner's provision instead (go-toolchain,
// node-toolchain), so the package is declared once, and a node without the owner refuses the module that
// needs it at resolution rather than failing at its first build. The owners must declare their packages.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
)
// Owners names, for each toolchain package, the one module that declares it.
var Owners = map[string]string{
"go": "go",
"nodejs": "nodejs",
"npm": "nodejs",
}
// Finding is one refusal: Module declares Package, which Owner alone may, or (Missing) Owner does not declare
// the Package it owns.
type Finding struct {
Module, Package, Owner string
Missing bool
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
declared := map[string]map[string]bool{} // module -> packages
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []struct {
Type string `json:"type"`
Package string `json:"package"`
} `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
declared[m.Module] = map[string]bool{}
for _, r := range m.Resources {
if r.Type != "package" {
continue
}
declared[m.Module][r.Package] = true
if owner, ok := Owners[r.Package]; ok && owner != m.Module {
out = append(out, Finding{Module: m.Module, Package: r.Package, Owner: owner})
}
}
}
var pkgs []string
for p := range Owners {
pkgs = append(pkgs, p)
}
sort.Strings(pkgs)
for _, p := range pkgs {
if !declared[Owners[p]][p] {
out = append(out, Finding{Module: Owners[p], Package: p, Owner: Owners[p], Missing: true})
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
if f.Missing {
fmt.Printf("%s does not declare the package %s it owns (novox/hq ADR 0477 clause 4)\n", f.Owner, f.Package)
continue
}
fmt.Printf("%s declares the package %s: only %s may; require its provision instead (novox/hq ADR 0477 clause 4)\n",
f.Module, f.Package, f.Owner)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("toolchains: go, nodejs and npm are declared only by their owners")
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func catalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for name, body := range manifests {
dir := filepath.Join(root, "modules", name)
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
}
func TestOnlyTheOwnerDeclaresAToolchainPackage(t *testing.T) {
root := catalogue(t, map[string]string{
"go": `{"module":"go","resources":[{"id":"go","type":"package","package":"go"}]}`,
"nodejs": `{"module":"nodejs","resources":[{"id":"nodejs","type":"package","package":"nodejs"},{"id":"npm","type":"package","package":"npm"}]}`,
"lab": `{"module":"lab","resources":[{"id":"git","type":"package","package":"git"},{"id":"go","type":"package","package":"go"}]}`,
"builder": `{"module":"builder","resources":[{"id":"n","type":"package","package":"nodejs"},{"id":"dir","type":"directory","path":"/go"}]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := []Finding{{Module: "builder", Package: "nodejs", Owner: "nodejs"}, {Module: "lab", Package: "go", Owner: "go"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("found %v, want %v", got, want)
}
}
func TestTheOwnersMustExistAndDeclareTheirPackages(t *testing.T) {
root := catalogue(t, map[string]string{
"go": `{"module":"go","resources":[]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 3 {
t.Errorf("an owner missing its packages gave %v, want three findings (go, nodejs, npm)", got)
}
}
// The catalogue as it stands: this is the check itself, run here too.
func TestTheCatalogueGivesEachToolchainPackageToItsOwnerAlone(t *testing.T) {
got, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("the catalogue declares a toolchain package outside its owner: %v", got)
}
}
+3 -3
View File
@@ -5,16 +5,16 @@ machine: an agent working on a change builds it and runs its tests there, with t
pull request. The lab is the QA step after that: it tests the work in a sandbox, through its seat at the mesh
scope, and is not where development happens.
Assign it to every node.
Assign it to every node, with the `go` and `nodejs` modules: it requires their provisions `go-toolchain` and `node-toolchain` (novox/hq ADR 0477 clause 4).
## Owns
| what | package | class (ADR 0182) |
|---|---|---|
| the Go toolchain, for `go build` and `go test` in every Go module | `go` | package |
| the Go toolchain, for `go build` and `go test` in every Go module | the `go` module's package `go` | package |
| a C compiler, for `go test -race` and cgo | `gcc` | package |
| make | `make` | package |
| Node.js and npm, for the TypeScript modules' `npm ci` and `npm test` | `nodejs`, `npm` | package |
| Node.js and npm, for the TypeScript modules' `npm ci` and `npm test` | the `nodejs` module's packages `nodejs`, `npm` | package |
| Python, for the scripts some checks run | `python` | package |
| git and jq | `git`, `jq` | package |
+1 -1
View File
@@ -1,6 +1,6 @@
# lab
The `lab` module of the mesh catalogue: installs the package `git`; installs the package `make`; installs the package `python`; installs the package `file`; installs the package `iproute2`; installs the package `npm`; installs the package `go`; installs the package `incus`.
The `lab` module of the mesh catalogue: installs the package `git`; installs the package `make`; installs the package `python`; installs the package `file`; installs the package `iproute2`; installs the package `incus`; requires `go-toolchain` and `node-toolchain`, so the `go` module and the `nodejs` module are assigned to a machine running it (novox/hq ADR 0477 clause 4).
## Files it does not know