Merge pull request 'No repository names a node: placement files, particulars and per-node build defaults go (issue 1126, delivery 2 of 2)' (#552) from feat/1126-placement-seat into main
This commit was merged in pull request #552.
This commit is contained in:
@@ -36,7 +36,6 @@ modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
||||
/modules/mesh-build-go/mesh-build-go
|
||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
||||
modules/mesh-config/cmd/mesh-config/mesh-config
|
||||
checks/placement/placement
|
||||
|
||||
# Built by their modules' build step (issue 1577), never committed.
|
||||
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-catalog/checks/noplacement
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,99 @@
|
||||
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
|
||||
//
|
||||
// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the
|
||||
// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses:
|
||||
//
|
||||
// 1. a `placement` directory (or file) at the root of the catalogue;
|
||||
//
|
||||
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
|
||||
//
|
||||
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
|
||||
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`.
|
||||
//
|
||||
// go run . <catalogue root>
|
||||
//
|
||||
// Exits 1 on a finding.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them.
|
||||
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"}
|
||||
|
||||
// Findings is every refusal in the catalogue at root, sorted.
|
||||
func Findings(root string) ([]string, error) {
|
||||
var out []string
|
||||
if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil {
|
||||
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §10)")
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
|
||||
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
|
||||
}
|
||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(manifests)
|
||||
for _, file := range manifests {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", file, err)
|
||||
}
|
||||
module := filepath.Base(filepath.Dir(file))
|
||||
for _, s := range named(doc, nil) {
|
||||
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// named collects the strings of a decoded manifest that start with one of the Prefixes.
|
||||
func named(v any, into []string) []string {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
for _, p := range Prefixes {
|
||||
if strings.HasPrefix(v, p) {
|
||||
return append(into, v)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, e := range v {
|
||||
into = named(e, into)
|
||||
}
|
||||
case map[string]any:
|
||||
for _, e := range v {
|
||||
into = named(e, into)
|
||||
}
|
||||
}
|
||||
return into
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: noplacement <catalogue root>")
|
||||
os.Exit(2)
|
||||
}
|
||||
found, err := Findings(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, f := range found {
|
||||
fmt.Println(f)
|
||||
}
|
||||
if len(found) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
want string // empty: clean
|
||||
}{
|
||||
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.assignments"]}`}, ""},
|
||||
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
|
||||
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
|
||||
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
|
||||
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
|
||||
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
|
||||
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for path, text := range row.files {
|
||||
p := filepath.Join(root, path)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := Findings(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
|
||||
t.Fatalf("findings = %q, want one containing %q", all, row.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2).
|
||||
func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
|
||||
got, err := Findings(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range got {
|
||||
t.Error(f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
||||
# the root, for a placement directory, and every manifest, for a read of the indexer's placement states.
|
||||
?placement
|
||||
modules/*/module.json
|
||||
@@ -2,8 +2,9 @@
|
||||
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
|
||||
// fixture of a covered module names a node of this one.
|
||||
//
|
||||
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
|
||||
// they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
|
||||
// The names are the mesh's own, not the repository's: the checker hands them in the environment value
|
||||
// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails
|
||||
// loudly, since it would pass on anything. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
|
||||
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
|
||||
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
|
||||
// before a line is read.
|
||||
@@ -34,14 +35,28 @@ func main() {
|
||||
if len(os.Args) > 1 {
|
||||
root = os.Args[1]
|
||||
}
|
||||
os.Exit(check(root))
|
||||
os.Exit(check(root, namesFromEnv()))
|
||||
}
|
||||
|
||||
// NamesEnv is the environment value that carries the mesh's node names (mesh-sdk delivery.NodeNamesEnv).
|
||||
const NamesEnv = "MESH_NODE_NAMES"
|
||||
|
||||
// namesFromEnv is the names NamesEnv holds, comma-separated, trimmed, empty entries left out.
|
||||
func namesFromEnv() []string {
|
||||
var out []string
|
||||
for _, n := range strings.Split(os.Getenv(NamesEnv), ",") {
|
||||
if n = strings.TrimSpace(n); n != "" {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
|
||||
func check(root string) int {
|
||||
names, err := readList(filepath.Join(root, "particulars"))
|
||||
if err == nil && len(names) == 0 {
|
||||
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
|
||||
func check(root string, names []string) int {
|
||||
var err error
|
||||
if len(names) == 0 {
|
||||
err = fmt.Errorf("no node names (%s is empty) — the check would pass on anything", NamesEnv)
|
||||
}
|
||||
var covered []string
|
||||
if err == nil {
|
||||
|
||||
@@ -74,26 +74,24 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
|
||||
func TestCheckReturnsOneOnAFindingAndTwoOnNoNames(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
write := func(p, s string) {
|
||||
p = filepath.Join(root, p)
|
||||
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||
os.WriteFile(p, []byte(s), 0o644)
|
||||
}
|
||||
write("particulars", "ace\n")
|
||||
write("checks/particulars/covered", "kernel\n")
|
||||
write("modules/kernel/a", "clean\n")
|
||||
if got := check(root); got != 0 {
|
||||
if got := check(root, []string{"ace"}); got != 0 {
|
||||
t.Errorf("clean = %d, want 0", got)
|
||||
}
|
||||
write("modules/kernel/a", "on ace\n")
|
||||
if got := check(root); got != 1 {
|
||||
if got := check(root, []string{"ace"}); got != 1 {
|
||||
t.Errorf("finding = %d, want 1", got)
|
||||
}
|
||||
os.Remove(filepath.Join(root, "particulars"))
|
||||
if got := check(root); got != 2 {
|
||||
t.Errorf("no list = %d, want 2", got)
|
||||
if got := check(root, nil); got != 2 {
|
||||
t.Errorf("no names = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +115,10 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
|
||||
|
||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
|
||||
func TestTheCoveredModulesNameNoNode(t *testing.T) {
|
||||
if code := check("../.."); code != 0 {
|
||||
if _, set := os.LookupEnv(NamesEnv); !set {
|
||||
t.Skip("ADR 0567 §16: this checker hands no node names (before D14 is delivered); the skip goes once it is")
|
||||
}
|
||||
if code := check("../..", namesFromEnv()); code != 0 {
|
||||
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
||||
# check("../.."): the root's list and every file of the modules it names.
|
||||
particulars
|
||||
# check("../.."): every file of the modules the covered list names. The node names come from the run (MESH_NODE_NAMES).
|
||||
modules
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module checks/placement
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f
|
||||
@@ -1,18 +0,0 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117 h1:88CNZOoQgu7YPYKW16qkWT8Gw9Ucg07aTrIImA2nYUk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f h1:mimAGERguQmCGYVNdRTrFcZ1dhpU6GWo+Q3EDhHOrVk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
@@ -1,80 +0,0 @@
|
||||
// Command placement is the catalogue's check of its placement files (novox/hq ADR 0549 §1, ADR 0555; issue 1126): every
|
||||
// `placement/<node>.json` reads as a placement of the node its name says, and the files together are refused, every
|
||||
// finding named at once, for two modules of one node on one host port, a pin, binding or holding naming a provider not
|
||||
// placed where it says, a seat held once for the mesh held twice, two nodes with one address on the private network, or
|
||||
// more than one hub (mesh-sdk delivery.CheckPlacements).
|
||||
//
|
||||
// A module the catalogue does not hold and a node not enrolled are the composition check's to refuse: placement names
|
||||
// modules of every repository the indexer reads and the nodes the mesh enrolled, neither of which this tree holds.
|
||||
//
|
||||
// go run . <catalogue root>
|
||||
//
|
||||
// Exits 1 on a finding.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Check reads every placement file under root and answers each finding; none is a catalogue whose placement holds. A
|
||||
// catalogue with no placement directory has none to check.
|
||||
func Check(root string) ([]string, error) {
|
||||
dir := filepath.Join(root, delivery.PlacementDir)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var found []string
|
||||
placements := map[string]delivery.Placement{}
|
||||
for _, e := range entries {
|
||||
rel := delivery.PlacementDir + "/" + e.Name()
|
||||
node, ok := delivery.PlacementFileNode(rel)
|
||||
if e.IsDir() || !ok {
|
||||
found = append(found, fmt.Sprintf("%s is not a placement file: the directory holds only <node>.json, one per node", rel))
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := delivery.DecodePlacement(raw)
|
||||
if err != nil {
|
||||
found = append(found, fmt.Sprintf("%s does not read as a placement: %v", rel, err))
|
||||
continue
|
||||
}
|
||||
if p.Revision != "" {
|
||||
found = append(found, fmt.Sprintf("%s names a revision, which only the indexer writes into the key", rel))
|
||||
}
|
||||
placements[node] = p
|
||||
}
|
||||
all := func(string) bool { return true }
|
||||
found = append(found, delivery.CheckPlacements(placements, all, all)...)
|
||||
sort.Strings(found)
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: placement <catalogue root>")
|
||||
os.Exit(2)
|
||||
}
|
||||
found, err := Check(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
for _, f := range found {
|
||||
fmt.Println(f)
|
||||
}
|
||||
if len(found) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The catalogue's placement files as the merge check reads them (ADR 0549 §1, ADR 0555): the real tree holds every
|
||||
// rule, and a tree that breaks them is refused with every finding named at once.
|
||||
func TestPlacementFiles(t *testing.T) {
|
||||
found, err := Check("../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range found {
|
||||
t.Errorf("%s (novox/hq ADR 0549 §1)", f)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
write := func(name, body string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Join(root, "placement"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "placement", name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if found, err := Check(root); err != nil || len(found) != 0 {
|
||||
t.Fatalf("a catalogue with no placement directory: %v, %v", found, err)
|
||||
}
|
||||
write("ace.json", `{"v":1,"node":"ace","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"a","ports":{"80":8080}},{"module":"b","ports":{"81":8080}}]}`)
|
||||
write("g14.json", `{"v":1,"node":"g14","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"c","bindings":{"pg":{"node":"ace","module":"pg"}}}]}`)
|
||||
write("novox.json", `{"v":1,"node":"shanks","modules":[]}`)
|
||||
write("bad.json", `{"v":1,"node":"bad","modules":{}}`)
|
||||
write("dated.json", `{"v":1,"node":"dated","revision":"abc","modules":[]}`)
|
||||
write("notes.md", "x")
|
||||
found, err = Check(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"a and b are both published on host port 8080", "bound to ace/pg", "both have the address 10.77.0.1",
|
||||
"ace, g14 are each placed as it", "placement file of novox names the node", "bad.json does not read", "dated.json names a revision",
|
||||
"notes.md is not a placement file"} {
|
||||
hit := false
|
||||
for _, f := range found {
|
||||
hit = hit || strings.Contains(f, want)
|
||||
}
|
||||
if !hit {
|
||||
t.Errorf("no finding says %q; found %v", want, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
||||
+3
-19
@@ -560,7 +560,7 @@ export class GiteaClient {
|
||||
* **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages
|
||||
* run past a bound): the call fails naming the repository and the commit, never answering the directories it did
|
||||
* see, since a directory left out would be taken for one without a module. Reads only. */
|
||||
async commitTrees(owner: string, repo: string, sha: string, paths?: Record<string, string[]>, listed?: string[]): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>>; listed?: Record<string, Record<string, string>> }> {
|
||||
async commitTrees(owner: string, repo: string, sha: string, paths?: Record<string, string[]>): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>> }> {
|
||||
const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`;
|
||||
const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`);
|
||||
const entries: any[] = [];
|
||||
@@ -587,23 +587,7 @@ export class GiteaClient {
|
||||
if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`);
|
||||
trees[dir] = tree;
|
||||
}
|
||||
// **Listed directories** (novox/hq ADR 0549 §2, issue 1126): each asked directory's files directly under it, by name
|
||||
// to blob id, from the same listing: how the forge bridge finds the catalogue's placement files, one per node, at a
|
||||
// merge. A directory the commit lacks lists nothing (the catalogue before its first placement file).
|
||||
let files: Record<string, Record<string, string>> | undefined;
|
||||
if (listed !== undefined) {
|
||||
files = {};
|
||||
for (const dir of listed) {
|
||||
files[dir] = {};
|
||||
const prefix = dir === "" ? "" : `${dir}/`;
|
||||
for (const e of entries) {
|
||||
if (e?.type !== "blob" || typeof e.path !== "string" || !e.path.startsWith(prefix)) continue;
|
||||
const name = e.path.slice(prefix.length);
|
||||
if (name !== "" && !name.includes("/")) files[dir][name] = String(e.sha);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (paths === undefined) return files === undefined ? { root, trees } : { root, trees, listed: files };
|
||||
if (paths === undefined) return { root, trees };
|
||||
// **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a
|
||||
// directory's, from the same listing, by directory and by path relative to it. A path the listing lacks fails
|
||||
// naming both, and nothing is answered in part.
|
||||
@@ -618,7 +602,7 @@ export class GiteaClient {
|
||||
named[dir][p] = id;
|
||||
}
|
||||
}
|
||||
return files === undefined ? { root, trees, paths: named } : { root, trees, paths: named, listed: files };
|
||||
return { root, trees, paths: named };
|
||||
}
|
||||
|
||||
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
||||
|
||||
@@ -183,15 +183,6 @@ test("named paths: each asked path's git object id at the commit, a file's or a
|
||||
});
|
||||
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
|
||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
|
||||
// Listed directories (ADR 0549 §2, issue 1126): the files directly under each, not deeper; a missing one lists none.
|
||||
const listed = await client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a", "modules/b", "", "placement"]);
|
||||
assert.deepEqual(listed.listed, {
|
||||
"modules/a": { "main.go": h("6"), "module.json": h("5") },
|
||||
"modules/b": {},
|
||||
"": { "README.md": h("1"), "module.json": h("2") },
|
||||
placement: {},
|
||||
});
|
||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).listed, undefined, "no directory asked, none listed");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
@@ -218,12 +209,7 @@ test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another
|
||||
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
|
||||
try {
|
||||
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
|
||||
assert.ok("paths" in tool.input && "listed" in tool.input, "the tool names the arguments");
|
||||
const listedAnswer: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: ["modules/d"] } as any);
|
||||
assert.deepEqual(listedAnswer.listed, { "modules/d": { "notes.md": h("b") } });
|
||||
for (const bad of ["modules/d", ["../x"], ["/abs"], [1]]) {
|
||||
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: bad } as any), /listed/, JSON.stringify(bad));
|
||||
}
|
||||
assert.ok("paths" in tool.input, "the tool names the arguments");
|
||||
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
|
||||
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
|
||||
const before = asked.length;
|
||||
|
||||
@@ -79,17 +79,6 @@ function namedPaths(raw: unknown): Record<string, string[]> | undefined {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The `listed` argument of gitea_commit_trees: [directory], each plain (no `.` or `..` segment, not absolute, the root
|
||||
* the empty name); anything else is refused before the forge is asked. Absent is absent. */
|
||||
function listedDirs(raw: unknown): string[] | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (!Array.isArray(raw) || raw.some((d) => typeof d !== "string")) throw new Error("listed is [directory, ...]");
|
||||
for (const d of raw as string[]) {
|
||||
if (d !== "" && d.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`listed: ${JSON.stringify(d)} is not a plain relative directory`);
|
||||
}
|
||||
return raw as string[];
|
||||
}
|
||||
|
||||
export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin, source: SourceRead = sourceFromBus(state)): ToolDefinition[] {
|
||||
return namesChecked(allGiteaTools(gitea, admin, source));
|
||||
}
|
||||
@@ -733,20 +722,19 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined
|
||||
},
|
||||
{
|
||||
name: "gitea_commit_trees",
|
||||
description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. With listed, [directory], the answer also carries listed, {directory: {name: git blob id}} of the files directly under each (the catalog's placement files, novox/hq ADR 0549 §2). A listing the forge cut is refused, never answered in part. Reads only.",
|
||||
description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.",
|
||||
input: {
|
||||
owner: { type: "string", description: "the repository owner" },
|
||||
repo: { type: "string", description: "the repository name" },
|
||||
sha: { type: "string", description: "the commit, its full 40-hex sha" },
|
||||
paths: { type: "object", description: "optional, {directory: [path relative to it]}: the git object id of each named file or directory is answered under paths; the root directory is the empty name" },
|
||||
listed: { type: "array", items: { type: "string" }, description: "optional, [directory]: the files directly under each, {directory: {name: git blob id}}, are answered under listed; a directory the commit lacks lists none" },
|
||||
},
|
||||
run: async (args) => {
|
||||
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
||||
const sha = String(args.sha ?? "").trim();
|
||||
if (!owner || !repo) throw new Error("name the repository: owner and repo");
|
||||
if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error("sha is a commit's full 40-hex sha");
|
||||
return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths), listedDirs(args.listed));
|
||||
return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths));
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -87,13 +87,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
||||
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||
// nothing new.
|
||||
var cores func() float64
|
||||
var hours func() (*buildstep.BuildWindow, string)
|
||||
if capacity == nil {
|
||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||
watch = buildstep.WatchConfigServed
|
||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskGo], m.Say)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -126,7 +127,8 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||
var doc struct {
|
||||
Config map[string]struct {
|
||||
Default json.RawMessage `json:"default"`
|
||||
Default json.RawMessage `json:"default"`
|
||||
Pattern, Description string
|
||||
} `json:"config"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &doc)
|
||||
@@ -144,10 +146,20 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
||||
}
|
||||
var capacity string
|
||||
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||
window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$")
|
||||
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||
if window.MatchString(value) != ok {
|
||||
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||
}
|
||||
}
|
||||
if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") {
|
||||
t.Errorf("the build-hours description names a node pair")
|
||||
}
|
||||
var capacity float64
|
||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
||||
if capacity != buildstep.DefaultCapacities {
|
||||
t.Errorf("the capacity's default = %q, want %q", capacity, buildstep.DefaultCapacities)
|
||||
if capacity != 0 {
|
||||
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||
}
|
||||
settings := resource(t, m, "settings")
|
||||
if settings["path"] != "${dir:state}/settings.json" {
|
||||
|
||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
||||
case m.Capacity != nil:
|
||||
slots = m.Capacity()
|
||||
case m.Settings != "":
|
||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
||||
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||
}
|
||||
return buildstep.NodeLimits(slots)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ const DigestVersion = "6"
|
||||
// depends on the siblings' commits.
|
||||
const besideVariable = "MESH_CHECK_BESIDE"
|
||||
|
||||
// namesVariable is the same for the mesh's node names: a package whose tests reach a file naming it depends on the node
|
||||
// list the ask carries, so its digest holds the sorted names and a new node misses a kept verdict (ADR 0567 §15).
|
||||
const namesVariable = "MESH_NODE_NAMES"
|
||||
|
||||
// ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a
|
||||
// repository's documents, a sibling directory), which the Go toolchain does not list (issue 1935). It lists, one per
|
||||
// line, what those tests read, as paths from the repository's root; a path may be a glob (filepath.Match), and a
|
||||
@@ -164,6 +168,8 @@ func (m *Module) digestTool() StepTool {
|
||||
"workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"},
|
||||
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
||||
"runner": runnerProperty,
|
||||
delivery.NodeNamesArg: map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||
"description": "optional, the names of the mesh's nodes: a package whose tests read " + delivery.NodeNamesEnv + " has them in its digest"},
|
||||
},
|
||||
"required": []string{"workspace", "level"},
|
||||
},
|
||||
@@ -178,6 +184,10 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := nodeNamesArg(args[delivery.NodeNamesArg])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runner == RunnerWorkspace {
|
||||
if err := m.workspaceThere(workspace); err != nil {
|
||||
return nil, err
|
||||
@@ -191,7 +201,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
||||
default:
|
||||
return nil, fmt.Errorf("the level %q is neither unit nor integration", level)
|
||||
}
|
||||
a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace})
|
||||
a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace, NodeNames: names})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -201,7 +211,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
||||
a.Digest.JudgedBy = judged
|
||||
return *a.Digest, nil
|
||||
}
|
||||
res, err := m.digestHere(ctx, workspace, level)
|
||||
res, err := m.digestHere(ctx, workspace, level, names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -210,7 +220,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
||||
}
|
||||
|
||||
// digestHere is the digest step as this program runs it.
|
||||
func (m *Module) digestHere(ctx context.Context, workspace, level string) (DigestResult, error) {
|
||||
func (m *Module) digestHere(ctx context.Context, workspace, level string, names []string) (DigestResult, error) {
|
||||
if !workspaceOK.MatchString(workspace) {
|
||||
return DigestResult{}, fmt.Errorf("%q is not a workspace id", workspace)
|
||||
}
|
||||
@@ -241,6 +251,14 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges
|
||||
besideHeader = append(besideHeader, "beside "+b)
|
||||
}
|
||||
|
||||
// The node names, sorted, as a header line for a package that reads them.
|
||||
sortedNames := append([]string{}, names...)
|
||||
sort.Strings(sortedNames)
|
||||
var namesHeader []string
|
||||
if len(sortedNames) > 0 {
|
||||
namesHeader = []string{"node-names " + strings.Join(sortedNames, ",")}
|
||||
}
|
||||
|
||||
dirs, err := m.goModDirs(ctx, tree)
|
||||
if err != nil {
|
||||
return DigestResult{}, err
|
||||
@@ -251,7 +269,7 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges
|
||||
if err != nil {
|
||||
return DigestResult{}, err
|
||||
}
|
||||
if err := digestModule(&res, pkgs, tree, header, besideHeader, level == LevelIntegration); err != nil {
|
||||
if err := digestModule(&res, pkgs, tree, header, besideHeader, namesHeader, level == LevelIntegration); err != nil {
|
||||
return DigestResult{}, err
|
||||
}
|
||||
}
|
||||
@@ -302,7 +320,7 @@ func parsePackages(out string) (map[string]*goPackage, error) {
|
||||
|
||||
// digestModule adds to res the digest of each root package of one go.mod directory. A root is a package of the main
|
||||
// module that is listed, not only a dependency, and not a variant of another.
|
||||
func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside []string, needs bool) error {
|
||||
func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside, names []string, needs bool) error {
|
||||
roots := []string{}
|
||||
for path, p := range pkgs {
|
||||
if p.ForTest == "" && !p.DepOnly && !p.Standard && !strings.HasSuffix(path, ".test") && p.Module != nil && p.Module.Main {
|
||||
@@ -349,7 +367,7 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
||||
res.neverKept[path] = why
|
||||
continue
|
||||
}
|
||||
d, err := packageDigest(root, testMain, pkgs, tree, header, beside, declared)
|
||||
d, err := packageDigest(root, testMain, pkgs, tree, header, beside, names, declared)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
@@ -361,9 +379,9 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
||||
// packageDigest is the digest of one package: a hash over a sorted text of the header, the files of every package its
|
||||
// tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of
|
||||
// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header
|
||||
// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's.
|
||||
// only when a file the tests reach names MESH_CHECK_BESIDE, and the node names likewise for MESH_NODE_NAMES. A package of the standard library is the Go version's.
|
||||
// The lines a package's ReadsOutsideMarker declares join them (declaredReads).
|
||||
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, declared []string) (string, error) {
|
||||
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, names, declared []string) (string, error) {
|
||||
reached := map[string]*goPackage{root.ImportPath: root}
|
||||
for _, dep := range testMain.Deps {
|
||||
if p := pkgs[dep]; p != nil {
|
||||
@@ -371,7 +389,7 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
||||
}
|
||||
}
|
||||
lines := append([]string{}, declared...)
|
||||
readsBeside := false
|
||||
readsBeside, readsNames := false, false
|
||||
for _, p := range reached {
|
||||
if p.Standard || p.Module == nil {
|
||||
continue
|
||||
@@ -390,9 +408,10 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
||||
return "", err
|
||||
}
|
||||
lines = append(lines, l)
|
||||
if !readsBeside {
|
||||
if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil && bytes.Contains(raw, []byte(besideVariable)) {
|
||||
readsBeside = true
|
||||
if !readsBeside || !readsNames {
|
||||
if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil {
|
||||
readsBeside = readsBeside || bytes.Contains(raw, []byte(besideVariable))
|
||||
readsNames = readsNames || bytes.Contains(raw, []byte(namesVariable))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +443,9 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
||||
if readsBeside {
|
||||
header = append(append([]string{}, header...), beside...)
|
||||
}
|
||||
if readsNames {
|
||||
header = append(append([]string{}, header...), names...)
|
||||
}
|
||||
h := sha256.New()
|
||||
for _, l := range append(append([]string{}, header...), lines...) {
|
||||
h.Write([]byte(l + "\n"))
|
||||
|
||||
@@ -172,8 +172,9 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
||||
root := &goPackage{ImportPath: "x/r", Dir: dir, Module: &goModule{Path: "x", Main: true}, GoFiles: []string{"r.go"}, TestGoFiles: []string{"r.go"}}
|
||||
testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}}
|
||||
pkgs[root.ImportPath] = root
|
||||
var names []string
|
||||
digest := func() string {
|
||||
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, nil)
|
||||
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, names, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -188,6 +189,20 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
||||
if d3 := digest(); d3 == d1 {
|
||||
t.Errorf("a new version left the digest as it was")
|
||||
}
|
||||
// A different node list misses the cache only for a package whose tests read MESH_NODE_NAMES (ADR 0567 §15).
|
||||
names = []string{"node-names a"}
|
||||
d4 := digest()
|
||||
names = []string{"node-names a", "node-names b"}
|
||||
if d5 := digest(); d5 != d4 {
|
||||
t.Errorf("a package that does not read the node names changed with them")
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "r.go"), []byte("package r\n// "+namesVariable+"\n"), 0o644)
|
||||
names = []string{"node-names a"}
|
||||
d6 := digest()
|
||||
names = []string{"node-names a", "node-names b"}
|
||||
if d7 := digest(); d7 == d6 {
|
||||
t.Errorf("a new node left the digest of a package that reads the node names as it was")
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1935, 1936: a package whose reads-outside marker lists nothing it can trust is never kept: its digest is one no
|
||||
|
||||
@@ -72,7 +72,7 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
|
||||
return nil, fmt.Errorf("the integration level's bus: %w", err)
|
||||
}
|
||||
defer stopBus()
|
||||
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace)...)
|
||||
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace, nil)...)
|
||||
// Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run
|
||||
// below finds them built. Whether they compile is for the run to say; its failure is not read here.
|
||||
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
|
||||
|
||||
@@ -43,6 +43,7 @@ type levelSpec struct {
|
||||
Run string `json:"run,omitempty"`
|
||||
Setup time.Duration `json:"setup"`
|
||||
Bound time.Duration `json:"bound"`
|
||||
NodeNames []string `json:"node-names,omitempty"`
|
||||
}
|
||||
|
||||
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
|
||||
@@ -221,14 +222,14 @@ func (m *Module) answer(ctx context.Context, spec levelSpec) levelAnswer {
|
||||
tree := filepath.Join(m.Work, spec.Workspace, "tree")
|
||||
switch spec.Step {
|
||||
case stepDigest:
|
||||
d, err := m.digestHere(ctx, spec.Workspace, spec.Level)
|
||||
d, err := m.digestHere(ctx, spec.Workspace, spec.Level, spec.NodeNames)
|
||||
if err != nil {
|
||||
return levelAnswer{Error: err.Error()}
|
||||
}
|
||||
return levelAnswer{Digest: &d}
|
||||
case stepUnit:
|
||||
res, err := m.runLevel(ctx, tree, levelOpts{workspace: spec.Workspace, level: LevelUnit, packages: spec.Packages, run: spec.Run,
|
||||
bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace)})
|
||||
bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace, spec.NodeNames)})
|
||||
if err != nil {
|
||||
return levelAnswer{Error: err.Error()}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Issue 1309: a test that runs a step in bubblewrap is proof only where it runs. Where a build module judges (it hands
|
||||
@@ -29,8 +32,12 @@ func TestAMissingToolSkipsExceptBubblewrapOnABuildNode(t *testing.T) {
|
||||
// The levels a build module judges hand the tests the build node mark, with the siblings they read.
|
||||
func TestEveryLevelHandsTheTestsTheBuildNodeMark(t *testing.T) {
|
||||
m := &Module{Work: t.TempDir()}
|
||||
if env := m.checkEnv("ws1"); !slices.Contains(env, BuildNodeEnv+"=1") {
|
||||
t.Errorf("env %v lacks %s=1", env, BuildNodeEnv)
|
||||
if env := m.checkEnv("ws1", nil); !slices.Contains(env, BuildNodeEnv+"=1") || slices.ContainsFunc(env, func(e string) bool { return strings.HasPrefix(e, delivery.NodeNamesEnv) }) {
|
||||
t.Errorf("env %v lacks %s=1, or names nodes when none were asked", env, BuildNodeEnv)
|
||||
}
|
||||
// And the mesh's node names, when the ask carried them (ADR 0567 §12).
|
||||
if env := m.checkEnv("ws1", []string{"a", "b"}); !slices.Contains(env, delivery.NodeNamesEnv+"=a,b") {
|
||||
t.Errorf("env %v lacks the node names", env)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -38,7 +39,9 @@ func (m *Module) testTool() StepTool {
|
||||
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
||||
"packages": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||
"description": "package patterns of each module, ./... when none"},
|
||||
"run": map[string]any{"type": "string", "description": "optional go test -run pattern: only the tests it matches run (a try's tests)"},
|
||||
"run": map[string]any{"type": "string", "description": "optional go test -run pattern: only the tests it matches run (a try's tests)"},
|
||||
delivery.NodeNamesArg: map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||
"description": "optional, the names of the mesh's nodes: the tests get them comma-separated in " + delivery.NodeNamesEnv + " (a check that keeps node names out of the repository reads them there)"},
|
||||
"runner": runnerProperty,
|
||||
},
|
||||
"required": []string{"workspace", "level"},
|
||||
@@ -78,12 +81,16 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := nodeNamesArg(args[delivery.NodeNamesArg])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res *TestResult
|
||||
switch {
|
||||
case runner == RunnerWorkspace && (level == LevelUnit || level == LevelIntegration):
|
||||
return m.testAsCandidate(ctx, workspace, level, packages, pattern)
|
||||
return m.testAsCandidate(ctx, workspace, level, packages, pattern, names)
|
||||
case level == LevelUnit:
|
||||
res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(workspace)})
|
||||
res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(workspace, names)})
|
||||
case level == LevelIntegration:
|
||||
res, err = m.integration(ctx, workspace, tree, packages, pattern)
|
||||
default:
|
||||
@@ -98,14 +105,14 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
||||
|
||||
// testAsCandidate runs a level as the candidate's program (ADR 0535 §1): the integration level is refused first as the
|
||||
// installed program would refuse it (no postgres), then the whole level runs in the candidate's one shut-off process.
|
||||
func (m *Module) testAsCandidate(ctx context.Context, workspace, level string, packages []string, pattern string) (*TestResult, error) {
|
||||
spec := levelSpec{Step: stepUnit, Workspace: workspace, Packages: packages, Run: pattern, Bound: m.unitBound()}
|
||||
func (m *Module) testAsCandidate(ctx context.Context, workspace, level string, packages []string, pattern string, names []string) (*TestResult, error) {
|
||||
spec := levelSpec{Step: stepUnit, Workspace: workspace, Packages: packages, Run: pattern, Bound: m.unitBound(), NodeNames: names}
|
||||
if level == LevelIntegration {
|
||||
if _, err := m.checkPostgres(ctx); err != nil {
|
||||
return nil, err // a *buildstep.Refusal: this level only
|
||||
}
|
||||
b := m.integrationBounds()
|
||||
spec = levelSpec{Step: stepIntegration, Workspace: workspace, Packages: packages, Run: pattern, Setup: b.Setup, Bound: b.Run}
|
||||
spec = levelSpec{Step: stepIntegration, Workspace: workspace, Packages: packages, Run: pattern, Setup: b.Setup, Bound: b.Run, NodeNames: names}
|
||||
}
|
||||
a, judged, err := m.asCandidate(ctx, spec)
|
||||
if err != nil {
|
||||
@@ -132,9 +139,35 @@ func (m *Module) besideEnv(workspace string) []string {
|
||||
// and present, so a test that needs it fails where it is missing instead of skipping (issue 1309).
|
||||
const BuildNodeEnv = "MESH_BUILD_NODE"
|
||||
|
||||
// checkEnv is what every level hands the tests it runs: the build node mark and the siblings (besideEnv).
|
||||
func (m *Module) checkEnv(workspace string) []string {
|
||||
return append([]string{BuildNodeEnv + "=1"}, m.besideEnv(workspace)...)
|
||||
// checkEnv is what every level hands the tests it runs: the build node mark, the siblings (besideEnv) and, when the
|
||||
// ask carried them, the mesh's node names (delivery.NodeNamesEnv, novox/hq ADR 0567 §12).
|
||||
func (m *Module) checkEnv(workspace string, names []string) []string {
|
||||
env := append([]string{BuildNodeEnv + "=1"}, m.besideEnv(workspace)...)
|
||||
if len(names) > 0 {
|
||||
env = append(env, delivery.NodeNamesEnv+"="+strings.Join(names, ","))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// nodeNamesArg reads the node-names argument: a list of names, absent when none.
|
||||
func nodeNamesArg(raw any) ([]string, error) {
|
||||
switch v := raw.(type) {
|
||||
case nil:
|
||||
return nil, nil
|
||||
case []string:
|
||||
return v, nil
|
||||
case []any:
|
||||
var out []string
|
||||
for _, e := range v {
|
||||
n, ok := e.(string)
|
||||
if !ok || n == "" || strings.ContainsAny(n, ", \n") {
|
||||
return nil, fmt.Errorf("%s is a list of node names, not %v", delivery.NodeNamesArg, e)
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s is a list of node names", delivery.NodeNamesArg)
|
||||
}
|
||||
|
||||
// packagesArg reads the packages argument: a list of package patterns, as []any when it came over the bus as JSON and
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
||||
"capacity": 0,
|
||||
"cores-per-step": 6,
|
||||
"build-hours": ""
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module meshbuildgo
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
],
|
||||
"config": {
|
||||
"capacity": {
|
||||
"type": "string",
|
||||
"description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||
"type": "number",
|
||||
"description": "How many asks of the Go build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"build-hours": {
|
||||
"type": "string",
|
||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
||||
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||
"default": ""
|
||||
},
|
||||
"cores-per-step": {
|
||||
|
||||
@@ -43,13 +43,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
||||
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||
// nothing new.
|
||||
var cores func() float64
|
||||
var hours func() (*buildstep.BuildWindow, string)
|
||||
if capacity == nil {
|
||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||
watch = buildstep.WatchConfigServed
|
||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskImage], m.Say)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
||||
case m.Capacity != nil:
|
||||
slots = m.Capacity()
|
||||
case m.Settings != "":
|
||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
||||
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||
}
|
||||
return buildstep.NodeLimits(slots)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -121,6 +123,30 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes
|
||||
if strings.TrimSpace(string(tpl)) != "${seat:mesh-artifact-store:reach}" {
|
||||
t.Errorf("registry template = %q", tpl)
|
||||
}
|
||||
raw, _ = os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||
var doc struct {
|
||||
Config map[string]struct {
|
||||
Default json.RawMessage `json:"default"`
|
||||
Pattern, Description string
|
||||
} `json:"config"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &doc)
|
||||
var capacity float64 = -1
|
||||
_ = json.Unmarshal(doc.Config["capacity"].Default, &capacity)
|
||||
if capacity != 0 {
|
||||
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||
}
|
||||
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||
window := regexp.MustCompile("^(?:" + doc.Config["build-hours"].Pattern + ")$")
|
||||
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||
if window.MatchString(value) != ok {
|
||||
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||
}
|
||||
}
|
||||
if strings.Contains(doc.Config["build-hours"].Description, "=") {
|
||||
t.Errorf("the build-hours description names a node pair")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Every tool the manifest names is served, and every tool served is named.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
||||
"capacity": 0,
|
||||
"cores-per-step": 2,
|
||||
"build-hours": ""
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module meshbuildimage
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe h1:QDWi8k7zxaRPnJ+V/izoy6A19hZt1LTLl3uksZSlqOg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
],
|
||||
"config": {
|
||||
"capacity": {
|
||||
"type": "string",
|
||||
"description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||
"type": "number",
|
||||
"description": "How many asks of the image build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"build-hours": {
|
||||
"type": "string",
|
||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
||||
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||
"default": ""
|
||||
},
|
||||
"cores-per-step": {
|
||||
|
||||
@@ -51,13 +51,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
||||
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||
// nothing new.
|
||||
var cores func() float64
|
||||
var hours func() (*buildstep.BuildWindow, string)
|
||||
if capacity == nil {
|
||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
||||
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||
watch = buildstep.WatchConfigServed
|
||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskNode], m.Say)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -84,7 +85,8 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||
var doc struct {
|
||||
Config map[string]struct {
|
||||
Default json.RawMessage `json:"default"`
|
||||
Default json.RawMessage `json:"default"`
|
||||
Pattern, Description string
|
||||
} `json:"config"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &doc)
|
||||
@@ -102,10 +104,20 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
||||
}
|
||||
var capacity string
|
||||
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||
window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$")
|
||||
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||
if window.MatchString(value) != ok {
|
||||
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||
}
|
||||
}
|
||||
if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") {
|
||||
t.Errorf("the build-hours description names a node pair")
|
||||
}
|
||||
var capacity float64
|
||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
||||
if capacity != buildstep.DefaultCapacities {
|
||||
t.Errorf("the capacity's default = %q", capacity)
|
||||
if capacity != 0 {
|
||||
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||
}
|
||||
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
||||
t.Error("no settings file")
|
||||
|
||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
||||
case m.Capacity != nil:
|
||||
slots = m.Capacity()
|
||||
case m.Settings != "":
|
||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
||||
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||
}
|
||||
return buildstep.NodeLimits(slots)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
||||
"capacity": 0,
|
||||
"cores-per-step": 2,
|
||||
"build-hours": ""
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module meshbuildnode
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
],
|
||||
"config": {
|
||||
"capacity": {
|
||||
"type": "string",
|
||||
"description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
||||
"type": "number",
|
||||
"description": "How many asks of the Node build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"build-hours": {
|
||||
"type": "string",
|
||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
||||
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||
"default": ""
|
||||
},
|
||||
"cores-per-step": {
|
||||
|
||||
@@ -21,9 +21,9 @@ const (
|
||||
ProposalsState = "proposals"
|
||||
|
||||
AssignmentsRead = "mesh-controller.assignments"
|
||||
// PlacementRead is the indexer's placement, read from the catalogue at each merge (novox/hq ADR 0549 §2, §3): each
|
||||
// PlacementRead is the seat mesh-placement's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
|
||||
// node's key `placement.<node>` with the modules placed there.
|
||||
PlacementRead = "mesh-delivery-indexer.placement"
|
||||
PlacementRead = "mesh-delivery-assigner.placement"
|
||||
DeclaredConfigRead = "mesh-controller.declared-config"
|
||||
PredefinedRead = "mesh-controller.predefined"
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ func (c *Config) sources() (Sources, error) {
|
||||
}); err != nil {
|
||||
return s, fmt.Errorf("the assignments cannot be read: %w", err)
|
||||
}
|
||||
// And every module the catalog's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both
|
||||
// And every module the seat's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both
|
||||
// sources stand (ADR 0549 §5), so a pair either names is resolved, and none loses its values while the two differ.
|
||||
// Placement that cannot be read yet (the indexer's state not on the bus) leaves the assignments alone, said.
|
||||
// Placement that cannot be read yet (the seat's state not on the bus) leaves the assignments alone, said.
|
||||
standing := map[string]bool{}
|
||||
for _, a := range s.Assignments {
|
||||
standing[a.Node+"."+a.Module] = true
|
||||
@@ -57,7 +57,7 @@ func (c *Config) sources() (Sources, error) {
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
problems = append(problems, fmt.Sprintf("the catalog's placement cannot be read, so the assignments alone are resolved: %v", err))
|
||||
problems = append(problems, fmt.Sprintf("the seat's placement cannot be read, so the assignments alone are resolved: %v", err))
|
||||
}
|
||||
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
|
||||
m, err := ParseModuleDeclarations(raw)
|
||||
|
||||
@@ -2,4 +2,4 @@ module mesh-config
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49 h1:h1hE+H1TzmeWNFP3EnfhUXTZq2gyiyeND32Tsg83ygY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.assignments",
|
||||
"mesh-delivery-indexer.placement",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-controller.declared-config",
|
||||
"mesh-controller.predefined"
|
||||
],
|
||||
|
||||
@@ -59,7 +59,7 @@ type checkerVerdict struct {
|
||||
Words string `json:"words,omitempty"`
|
||||
} `json:"failed,omitzero"`
|
||||
// Composition is what the check of the machines' composition made of it (mesh-sdk delivery.Composition): its state
|
||||
// not-run says the composition, and with it a placement change's answers from its nodes, was not judged.
|
||||
// not-run says the composition was not judged.
|
||||
Composition *struct {
|
||||
State string `json:"state"`
|
||||
Words string `json:"words,omitempty"`
|
||||
@@ -341,7 +341,7 @@ func (h *Holder) checkAt(members []*Delivery) checkSaid {
|
||||
if ok {
|
||||
switch v.State {
|
||||
case checkerPass:
|
||||
return checkSaid{Key: checkSaidPass, Words: "passed its check" + notComposed(v, members)}
|
||||
return checkSaid{Key: checkSaidPass, Words: "passed its check" + notComposed(v)}
|
||||
case checkerRunning:
|
||||
words := "its check has no verdict yet: it is running"
|
||||
if since, held := h.runningSince[hash]; held && !since.IsZero() {
|
||||
@@ -384,20 +384,13 @@ func (h *Holder) checkAt(members []*Delivery) checkSaid {
|
||||
return checkSaid{Key: checkSaidNone, Words: "its check has no verdict yet"}
|
||||
}
|
||||
|
||||
// notComposed is what a passing verdict adds when its composition was not judged (mesh-sdk delivery.CompositionNotRun): a
|
||||
// placement change's pass then holds no node's answer to its placement (ADR 0559 §5), which the ask says. Empty otherwise.
|
||||
func notComposed(v checkerVerdict, members []*Delivery) string {
|
||||
// notComposed is what a passing verdict adds when its composition was not judged (mesh-sdk delivery.CompositionNotRun).
|
||||
// Empty otherwise.
|
||||
func notComposed(v checkerVerdict) string {
|
||||
if v.Composition == nil || v.Composition.State != sdk.CompositionNotRun {
|
||||
return ""
|
||||
}
|
||||
why := firstOf(oneLine(v.Composition.Words), "it was not run")
|
||||
for _, m := range members {
|
||||
for _, p := range m.Paths {
|
||||
if strings.HasPrefix(p, "placement/") && strings.HasSuffix(p, ".json") {
|
||||
return ", but its composition was not judged (" + why + "), so no node answered its placement"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ", but its composition was not judged (" + why + ")"
|
||||
}
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ const waitsCheckBeforeAsking = "waits its check before asking the operator's App
|
||||
// has no verdict (the operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request").
|
||||
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and so is a check that has not
|
||||
// answered within askCheckBound of the heads becoming ready, which the ask says. For a group the check is the one of all its
|
||||
// heads together; for a placement change it ends only once every node it places answered (ADR 0559 §5). Under the lock.
|
||||
// heads together. Under the lock.
|
||||
func (h *Holder) verdictWaits(d *Delivery) string {
|
||||
members := h.askMembers(d)
|
||||
c := h.checkAt(members)
|
||||
|
||||
@@ -75,12 +75,11 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
// past which it is asked anyway, saying so, and asked again in new words once the check passes. The verdict is the
|
||||
// checker's verdicts bucket value as captured on the bus (mesh-delivery-checker, `<group>.<heads hash>`).
|
||||
for _, row := range []struct {
|
||||
name, state, failed, then, path string
|
||||
gated bool
|
||||
notComposed bool
|
||||
after time.Duration
|
||||
asks, open int
|
||||
want, not []string
|
||||
name, state, failed, then string
|
||||
gated bool
|
||||
after time.Duration
|
||||
asks, open int
|
||||
want, not []string
|
||||
}{
|
||||
{name: "gated, passed", gated: true, state: "pass", asks: 1,
|
||||
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
||||
@@ -100,9 +99,6 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running (since 15:00 CEST); asked anyway at 16:00 CEST"}},
|
||||
{name: "ungated, passed, its verdict gone past its bound", state: "pass", after: askCheckBound, then: "gone", asks: 1, open: -1,
|
||||
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet; asked anyway at 16:00 CEST"}},
|
||||
// A placement change's pass with its composition not judged holds no node's answer, and the ask says so (ADR 0559 §5).
|
||||
{name: "ungated, placement passed uncomposed", state: "pass", path: "placement/node-a.json", notComposed: true, asks: 1,
|
||||
want: []string{"passed its check, but its composition was not judged (the assigner is not running), so no node answered its placement"}},
|
||||
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
|
||||
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
||||
} {
|
||||
@@ -112,7 +108,7 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
ungated(v)
|
||||
}
|
||||
v.op.auto = false
|
||||
p := pr("novox/app", 7, readyHead, "feat/x", firstOf(row.path, "src/a.go"))
|
||||
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
|
||||
p.Title = "Fix it"
|
||||
v.h.PullUpdated(p)
|
||||
head := headOf("novox/app", 7, readyHead)
|
||||
@@ -121,11 +117,7 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
}
|
||||
v.settleAll()
|
||||
if !row.gated && row.state != "" {
|
||||
var more map[string]any
|
||||
if row.notComposed {
|
||||
more = map[string]any{"composition": map[string]string{"state": "not-run", "words": "the assigner is not running"}}
|
||||
}
|
||||
checkerSaysWith(v, "g", row.state, row.failed, more, head)
|
||||
checkerSaysWith(v, "g", row.state, row.failed, nil, head)
|
||||
v.settleAll()
|
||||
}
|
||||
if row.after > 0 {
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-gate
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026211913-68d65e6bbf61 h1:/c6ncICWjq55nBf8TlDM0ROe/PuH+qZIdnuVbtg8UuM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026211913-68d65e6bbf61/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f h1:mimAGERguQmCGYVNdRTrFcZ1dhpU6GWo+Q3EDhHOrVk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -437,10 +437,10 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
||||
later -= time.Hour
|
||||
// own_drift reads it from the node's latest shadow answer, mesh-wide; the id as a node runs it, its module's name
|
||||
// first (captured from novox's shadow answer, 2026-10-28: only-office.needs-jwt).
|
||||
answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{
|
||||
answer := delivery.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{
|
||||
Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password",
|
||||
Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}}
|
||||
have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": answer},
|
||||
have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"pro": answer},
|
||||
Composes: delivery.ComposesController})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -453,7 +453,7 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
||||
}
|
||||
if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" ||
|
||||
drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable ||
|
||||
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["g"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController {
|
||||
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController {
|
||||
t.Errorf("own_drift: %+v", drift)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-vault
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2 h1:eSwObLdxs3jxPrlcwdaMo9hzIDTUz2ogWw8cjX8To7I=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# This mesh's node names (novox/hq ADR 0507 §9, issue 1838): the one place they may be written.
|
||||
novox
|
||||
ace
|
||||
shanks
|
||||
g14
|
||||
Reference in New Issue
Block a user