diff --git a/checks/toolchains/go.mod b/checks/toolchains/go.mod new file mode 100644 index 00000000..63d6ec97 --- /dev/null +++ b/checks/toolchains/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/toolchains + +go 1.22 diff --git a/checks/toolchains/main.go b/checks/toolchains/main.go new file mode 100644 index 00000000..8954a5c6 --- /dev/null +++ b/checks/toolchains/main.go @@ -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 . +// +// 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") +} diff --git a/checks/toolchains/main_test.go b/checks/toolchains/main_test.go new file mode 100644 index 00000000..b80cd58e --- /dev/null +++ b/checks/toolchains/main_test.go @@ -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) + } +} diff --git a/modules/dev-tools/README.md b/modules/dev-tools/README.md index 35017a93..64eb8d9a 100644 --- a/modules/dev-tools/README.md +++ b/modules/dev-tools/README.md @@ -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 | diff --git a/modules/lab/README.md b/modules/lab/README.md index 17db440e..6ce0f8d5 100644 --- a/modules/lab/README.md +++ b/modules/lab/README.md @@ -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