Judge a recorded build, not build-agent's outcome, and read a registration's warrant from the merge a key names, so a done key registers under the same rules (issue 1851, A4a)

This commit is contained in:
2026-10-22 19:05:04 +02:00
parent eff0d5cbf4
commit f00b6aff8b
5 changed files with 161 additions and 71 deletions
+101 -53
View File
@@ -679,13 +679,6 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
recorded.Ref = followedBranch(was.Ref) recorded.Ref = followedBranch(was.Ref)
} }
} }
if err := namesNoInstallation(manifest); err != nil {
return manifest, kept, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
result.On, result.Repository, short(result.Commit), err)
}
// **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk
// below is the trunk of whatever repository was built, which may be one an agent made — and a module named
// `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal.
trunk := result.Trunk trunk := result.Trunk
if was, err := inv.SourceOf(ctx, manifest.Module); err == nil && followedBranch(was.Ref) != "" { if was, err := inv.SourceOf(ctx, manifest.Module); err == nil && followedBranch(was.Ref) != "" {
trunk = followedBranch(was.Ref) trunk = followedBranch(was.Ref)
@@ -693,55 +686,111 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
if trunk == "" { if trunk == "" {
trunk = "main" trunk = "main"
} }
repoID, err := mayRegisterFrom(ctx, inv, manifest.Module, recorded, result.ID, result.Path, trunk) j := takenBuild{kept: kept, manifest: manifest, recorded: recorded, ask: byBuild(result.ID), path: result.Path,
trunk: trunk, by: result.On, what: result.Repository, commit: result.Commit}
// The checks only an outcome of build-agent needs: a key is a trunk build by construction (ADR 0501 §2), in no
// scratch namespace, and carries no list of its own.
j.published = func() error {
// **Only a commit on the trunk is published** (novox/hq ADR 0238): a commit off its repository's
// default branch — a pull request's head, a feature branch built by hand, a `rebuild` or `replay
// --register` of one — is for checking, and is never a module's version; nothing could then send it.
// The branch the module already follows is its trunk — never the branch a build was asked at, or a
// build of a feature branch by name would make that branch its trunk.
follows := ""
if was, err := inv.SourceOf(ctx, manifest.Module); err == nil {
follows = followedBranch(was.Ref)
}
if err := publishable(result, follows); err != nil {
return err
}
// **Never a build in the scratch namespace** (novox/hq ADR 0239 §9, ADR 0350 §2): what a check builds is
// registered only as the walk's copy in the module's own namespace.
if scratch := inScratch(result.Made); scratch != "" {
return fmt.Errorf("%s is in the scratch namespace, which only a walk's copy leaves (ADR 0350)", scratch)
}
return nil
}
j.carried = func() error {
// **The newest trunk build's list of predefined keys is published** (novox/hq ADR 0348 §7): a build of the
// catalogue that may be registered keeps its list when its commit is newer than the one kept.
carried, err := carriedList(result)
if err != nil {
return err
}
if carried != nil {
if err := publishPredefined(ctx, inv, result, carried); err != nil {
fmt.Printf("%s: the list of predefined keys it carried is not published: %v\n", result.ID, err)
}
}
return nil
}
manifest, err = registerJudged(ctx, inv, j)
return manifest, kept, err
}
// takenBuild is a build the controller judges and registers, whatever told it of the build: build-agent's outcome (the
// builder's own module's, until R4g, ADR 0453 §5) or the builder's done key (ADR 0501 §1). **One judging for both**:
// the rules of who may register, the gate a build failed, the predefined keys and the rule that an unchanged build
// source moves nothing are the same whichever part built it (ADR 0501 §1, §3).
type takenBuild struct {
kept inventory.Build
manifest catalogue.Manifest
recorded inventory.Source
ask registrationAsk
path string
trunk string
// by and what name the build in what is said: who built it, and what was built ("ace", "novox/mesh-catalog").
by, what, commit string
// published refuses a build that may not become a module's version (off the trunk, in the scratch namespace);
// nil for a key, which is a trunk fingerprint's by construction (ADR 0501 §2).
published func() error
// carried publishes a list of predefined keys the build carried; nil when it carried none.
carried func() error
}
// registerJudged judges a recorded build and registers its module, or says why not.
func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild) (catalogue.Manifest, error) {
manifest, kept := j.manifest, j.kept
notRegistered := func(err error) (catalogue.Manifest, error) {
return manifest, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", j.by, manifest.Module,
short(j.commit), err)
}
if err := namesNoInstallation(manifest); err != nil {
return manifest, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
j.by, j.what, short(j.commit), err)
}
// **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk
// below is the trunk of whatever repository was built, which may be one an agent made — and a module named
// `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal.
repoID, err := mayRegisterFrom(ctx, inv, manifest.Module, j.recorded, j.ask, j.path, j.trunk)
if err != nil { if err != nil {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", result.On, return notRegistered(err)
manifest.Module, short(result.Commit), err)
} }
// **Only a commit on the trunk is published** (novox/hq ADR 0238): a commit off its repository's if j.published != nil {
// default branch — a pull request's head, a feature branch built by hand, a `rebuild` or `replay if err := j.published(); err != nil {
// --register` of one — is for checking, and is never a module's version; nothing could then send it. return notRegistered(err)
// The branch the module already follows is its trunk — never the branch a build was asked at, or a }
// build of a feature branch by name would make that branch its trunk.
follows := ""
if was, err := inv.SourceOf(ctx, manifest.Module); err == nil {
follows = followedBranch(was.Ref)
}
if err := publishable(result, follows); err != nil {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", result.On,
manifest.Module, short(result.Commit), err)
}
// **Never a build in the scratch namespace** (novox/hq ADR 0239 §9, ADR 0350 §2): what a check builds is
// registered only as the walk's copy in the module's own namespace.
if scratch := inScratch(result.Made); scratch != "" {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %s is in the scratch "+
"namespace, which only a walk's copy leaves (ADR 0350)", result.On, manifest.Module, short(result.Commit), scratch)
} }
// **A build that failed its gate is never registered again** (novox/hq ADR 0236): an outcome heard // **A build that failed its gate is never registered again** (novox/hq ADR 0236): an outcome heard
// twice, or replayed, would otherwise make the build a rollback put back what the module is again, // twice, or replayed, would otherwise make the build a rollback put back what the module is again,
// and the next push would send it. // and the next push would send it.
if failed, err := inv.GateFailed(ctx, kept.ID); err != nil { if failed, err := inv.GateFailed(ctx, kept.ID); err != nil {
return manifest, kept, err return manifest, err
} else if failed { } else if failed {
return manifest, kept, fmt.Errorf("%s built %s (%s), which failed its gate on its first machine and was put "+ return manifest, fmt.Errorf("%s built %s (%s), which failed its gate on its first machine and was put "+
"back: it is recorded and not registered again — a newer build is", result.On, manifest.Module, "back: it is recorded and not registered again — a newer build is", j.by, manifest.Module,
short(result.Commit)) short(j.commit))
} }
// **The newest trunk build's list of predefined keys is published** (novox/hq ADR 0348 §7): a build of the // Then the module is judged against the list the mesh publishes, as every manifest read again is: one naming a
// catalogue that may be registered keeps its list when its commit is newer than the one kept. Then the module // key that list does not hold, or of another type, is refused here and never enters the catalogue.
// is judged against the list the mesh publishes, as every manifest read again is: one naming a key that list if j.carried != nil {
// does not hold, or of another type, is refused here and never enters the catalogue. if err := j.carried(); err != nil {
if carried, err := carriedList(result); err != nil { return manifest, err
return manifest, kept, err
} else if carried != nil {
if err := publishPredefined(ctx, inv, result, carried); err != nil {
fmt.Printf("%s: the list of predefined keys it carried is not published: %v\n", result.ID, err)
} }
} }
if manifest.NamesPredefined() { if manifest.NamesPredefined() {
if _, err := catalogue.ParseManifest(kept.Manifest); err != nil { if _, err := catalogue.ParseManifest(kept.Manifest); err != nil {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: judged against the "+ return notRegistered(fmt.Errorf("judged against the list of predefined keys the mesh publishes, %w", err))
"list of predefined keys the mesh publishes, %w", result.On, manifest.Module, short(result.Commit), err)
} }
} }
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from // **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
@@ -751,35 +800,34 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
if kept.SourceFingerprint != "" { if kept.SourceFingerprint != "" {
stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID) stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID)
if err != nil { if err != nil {
return manifest, kept, err return manifest, err
} }
if stands != "" && stands != kept.ID && len(raw) > 0 { if stands != "" && stands != kept.ID && len(raw) > 0 {
if same, err := catalogue.ParseManifest(raw); err == nil && same.Module == manifest.Module { if was, err := catalogue.ParseManifest(raw); err == nil && was.Module == manifest.Module {
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n", fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
manifest.Module, short(result.Commit), stands) manifest.Module, short(j.commit), stands)
manifest = same manifest = was
} }
} }
} }
if err := inv.RegisterModule(ctx, manifest, recorded); err != nil { if err := inv.RegisterModule(ctx, manifest, j.recorded); err != nil {
if errors.Is(err, inventory.ErrSuperseded) { if errors.Is(err, inventory.ErrSuperseded) {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", return notRegistered(err)
result.On, manifest.Module, short(result.Commit), err)
} }
return manifest, kept, err return manifest, err
} }
// A bus process the new definition gains gets its account now, on each machine assigned the module, so the // A bus process the new definition gains gets its account now, on each machine assigned the module, so the
// next push composes a real account (issue 1226). // next push composes a real account (issue 1226).
issueGainedProcessAccounts(ctx, inv, manifest) issueGainedProcessAccounts(ctx, inv, manifest)
// Which repository it is registered from, by the forge's own id (novox/hq ADR 0266). // Which repository it is registered from, by the forge's own id (novox/hq ADR 0266).
if err := inv.SetSourceIdentity(ctx, manifest.Module, repoID); err != nil { if err := inv.SetSourceIdentity(ctx, manifest.Module, repoID); err != nil {
return manifest, kept, err return manifest, err
} }
// The keep set just moved, and new bytes just landed (novox/hq ADR 0189). Asked here rather // The keep set just moved, and new bytes just landed (novox/hq ADR 0189). Asked here rather
// than on a timer of its own: this is the only moment either is true. Never fatal — the build // than on a timer of its own: this is the only moment either is true. Never fatal — the build
// worked and the module is registered. // worked and the module is registered.
collect(ctx, inv) collect(ctx, inv)
return manifest, kept, nil return manifest, nil
} }
// inScratch is the first artifact a build made into the scratch namespace; empty when none is. // inScratch is the first artifact a build made into the scratch namespace; empty when none is.
+42 -7
View File
@@ -469,8 +469,39 @@ func (f *sourceForms) onTheForge(repository, seat string) (owner, name string, o
// repository and path; or, for the build of a merge, when mesh-delivery announced it on the operator's Approve of // repository and path; or, for the build of a merge, when mesh-delivery announced it on the operator's Approve of
// that head (novox/hq ADR 0420 §7 for a moved module, ADR 0432 for a new one), the forge's checks still holding. path is the module's directory as built; branch the trunk it is registered from. // that head (novox/hq ADR 0420 §7 for a moved module, ADR 0432 for a new one), the forge's checks still holding. path is the module's directory as built; branch the trunk it is registered from.
func mayRegisterFrom(ctx context.Context, inv *inventory.Inventory, module string, built inventory.Source, func mayRegisterFrom(ctx context.Context, inv *inventory.Inventory, module string, built inventory.Source,
buildID, path, branch string) (int64, error) { ask registrationAsk, path, branch string) (int64, error) {
return mayRegisterIn(ctx, inv, newSourceForms(ctx, inv), module, built, buildID, path, branch) return mayRegisterIn(ctx, inv, newSourceForms(ctx, inv), module, built, ask, path, branch)
}
// registrationAsk is what a registration's warrant is read from (novox/hq ADR 0501 §3): the build an outcome answers,
// whose kept request says whether it was asked at the terminal or for a merge (build-agent's outcome, the builder's own
// module until R4g); or, for a take-in from the builder's done key, the merge the indexer's trunk value names and
// whether the operator asked the registration at the controller's terminal (ADR 0501 §4). A key asks no build, so it
// keeps no request.
type registrationAsk struct {
// Build is the build whose kept request is read; empty for a take-in from a key.
Build string
// Merge is the commit of the merge a key's module is registered from (the indexer's trunk value); empty for an
// outcome, whose request names it.
Merge string
// Terminal is a key registration the operator asked at the controller's terminal for this repository and path.
Terminal bool
}
// byBuild is the warrant of a build's outcome: read from the request kept under its id.
func byBuild(id string) registrationAsk { return registrationAsk{Build: id} }
// String is the ask as the registration's words name it.
func (a registrationAsk) String() string {
switch {
case a.Build != "":
return a.Build
case a.Terminal:
return "the registration asked at the terminal"
case a.Merge != "":
return fmt.Sprintf("the merge %.8s", a.Merge)
}
return "a key"
} }
// catalogueReader is the catalogue's manifests, as the router's records are found in them. // catalogueReader is the catalogue's manifests, as the router's records are found in them.
@@ -490,15 +521,19 @@ type registrations interface {
// mayRegisterIn is mayRegisterFrom on what it reads of the mesh handed in. // mayRegisterIn is mayRegisterFrom on what it reads of the mesh handed in.
func mayRegisterIn(ctx context.Context, inv registrations, forms *sourceForms, module string, built inventory.Source, func mayRegisterIn(ctx context.Context, inv registrations, forms *sourceForms, module string, built inventory.Source,
buildID, path, branch string) (int64, error) { ask registrationAsk, path, branch string) (int64, error) {
buildID := ask.String()
was, err := inv.SourceOf(ctx, module) was, err := inv.SourceOf(ctx, module)
isNew := errors.Is(err, inventory.ErrNoSuchModule) isNew := errors.Is(err, inventory.ErrNoSuchModule)
if err != nil && !isNew { if err != nil && !isNew {
return 0, err return 0, err
} }
terminal, err := askedHereFor(ctx, inv, forms, buildID, built, path) terminal := ask.Terminal
if err != nil { if ask.Build != "" {
return 0, err var err error
if terminal, err = askedHereFor(ctx, inv, forms, ask.Build, built, path); err != nil {
return 0, err
}
} }
refuse := func(why string) (int64, error) { refuse := func(why string) (int64, error) {
return 0, fmt.Errorf("%w: %s. A module is registered from such a source only by a build asked at the "+ return 0, fmt.Errorf("%w: %s. A module is registered from such a source only by a build asked at the "+
@@ -542,7 +577,7 @@ func mayRegisterIn(ctx context.Context, inv registrations, forms *sourceForms, m
// The forge's protection and the repository's identity are checked below as for every registration. // The forge's protection and the repository's identity are checked below as for every registration.
movedWhy := "" movedWhy := ""
if why != "" && !terminal { if why != "" && !terminal {
accepted, err := approvedMove(ctx, inv, forms, buildID, built) accepted, err := approvedMove(ctx, inv, forms, ask, built)
switch { switch {
case err == nil: case err == nil:
movedWhy, why = accepted, "" movedWhy, why = accepted, ""
+2 -2
View File
@@ -64,7 +64,7 @@ func registerFrom(t *testing.T, forms *sourceForms) (int64, error) {
mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true, mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true,
request: inventory.BuildRequest{ID: "build-1", Repository: aRepoOnTheForge, For: "build"}}, aRepoOnTheForge} request: inventory.BuildRequest{ID: "build-1", Repository: aRepoOnTheForge, For: "build"}}, aRepoOnTheForge}
return mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-view", inventory.Source{Repository: aRepoOnTheForge}, return mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-view", inventory.Source{Repository: aRepoOnTheForge},
"build-1", "", "main") byBuild("build-1"), "", "main")
} }
// 1277/AC2: an address unread twice, then read, is waited out and the forge is asked. // 1277/AC2: an address unread twice, then read, is waited out and the forge is asked.
@@ -112,7 +112,7 @@ func TestAKnownAddressAndAnotherHostIsStillNotOnTheForge(t *testing.T) {
const elsewhere = "http://elsewhere.example/x/y.git" const elsewhere = "http://elsewhere.example/x/y.git"
mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true, request: inventory.BuildRequest{ID: "build-1", For: "build"}}, elsewhere} mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true, request: inventory.BuildRequest{ID: "build-1", For: "build"}}, elsewhere}
_, err := mayRegisterIn(t.Context(), mesh, sourceFormsRead(context.Background(), time.Now, read), "mesh-delivery-view", _, err := mayRegisterIn(t.Context(), mesh, sourceFormsRead(context.Background(), time.Now, read), "mesh-delivery-view",
inventory.Source{Repository: elsewhere}, "build-1", "", "main") inventory.Source{Repository: elsewhere}, byBuild("build-1"), "", "main")
if !errors.Is(err, errNotItsSource) || !strings.Contains(err.Error(), "is not on the mesh's forge") { if !errors.Is(err, errNotItsSource) || !strings.Contains(err.Error(), "is not on the mesh's forge") {
t.Fatalf("1277/AC4: the refusal changed: %v", err) t.Fatalf("1277/AC4: the refusal changed: %v", err)
} }
+14 -7
View File
@@ -265,25 +265,32 @@ var errNotAMerge = errors.New("the build is not of a merge")
// (ADR 0420 §7): the build is of a merge, that merge is a pull request mesh-delivery announced it made, and the // (ADR 0420 §7): the build is of a merge, that merge is a pull request mesh-delivery announced it made, and the
// ask it made it on holds an Approve by the operator of that head. Answers why it was accepted; an error says why // ask it made it on holds an Approve by the operator of that head. Answers why it was accepted; an error says why
// it was not, and the refusal of ADR 0266 stands. // it was not, and the refusal of ADR 0266 stands.
func approvedMove(ctx context.Context, inv registrations, forms *sourceForms, buildID string, func approvedMove(ctx context.Context, inv registrations, forms *sourceForms, ask registrationAsk,
built inventory.Source) (string, error) { built inventory.Source) (string, error) {
r, found, err := inv.BuildRequestByID(ctx, buildID) commit := ask.Merge
if err != nil { if ask.Build != "" {
return "", err r, found, err := inv.BuildRequestByID(ctx, ask.Build)
if err != nil {
return "", err
}
if !found || r.For != "merge" || r.Commit == "" {
return "", errNotAMerge
}
commit = r.Commit
} }
if !found || r.For != "merge" || r.Commit == "" { if commit == "" {
return "", errNotAMerge return "", errNotAMerge
} }
owner, name, onForge := forms.onTheForge(built.Repository, built.Seat) owner, name, onForge := forms.onTheForge(built.Repository, built.Seat)
if !onForge { if !onForge {
return "", errors.New("the repository is not on the mesh's forge") return "", errors.New("the repository is not on the mesh's forge")
} }
merged, known, err := inv.MergeOf(ctx, owner+"/"+name, r.Commit) merged, known, err := inv.MergeOf(ctx, owner+"/"+name, commit)
if err != nil { if err != nil {
return "", err return "", err
} }
if !known { if !known {
return "", fmt.Errorf("the controller kept no merge of %s/%s at %.8s", owner, name, r.Commit) return "", fmt.Errorf("the controller kept no merge of %s/%s at %.8s", owner, name, commit)
} }
return approvedAnnounced(ctx, inv, owner, name, announcedOf(merged)) return approvedAnnounced(ctx, inv, owner, name, announcedOf(merged))
} }
@@ -116,7 +116,7 @@ func TestANewModuleIsRegisteredOnTheOperatorsApproveOfItsMergeAndRefusedOtherwis
if c.built != nil { if c.built != nil {
src = *c.built src = *c.built
} }
id, err := mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-view", src, "build-1", "", "main") id, err := mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-view", src, byBuild("build-1"), "", "main")
switch { switch {
case c.accepted && err != nil: case c.accepted && err != nil:
t.Fatalf("%s: the Approved merge's new module was refused: %v", c.ac, err) t.Fatalf("%s: the Approved merge's new module was refused: %v", c.ac, err)
@@ -157,7 +157,7 @@ func TestASecondNewModuleFromARepositoryIsJudgedAlongsideAndAsksNoWarrant(t *tes
request: inventory.BuildRequest{ID: "build-2", Repository: "novox/mesh-delivery", Seat: "git", For: "build"}}} request: inventory.BuildRequest{ID: "build-2", Repository: "novox/mesh-delivery", Seat: "git", For: "build"}}}
forms := &sourceForms{bases: map[string]string{}, base: func(string) string { return "http://forge.internal:20000" }} forms := &sourceForms{bases: map[string]string{}, base: func(string) string { return "http://forge.internal:20000" }}
built := inventory.Source{Repository: "novox/mesh-delivery", Seat: "git"} built := inventory.Source{Repository: "novox/mesh-delivery", Seat: "git"}
if id, err := mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-watchman", built, "build-2", "", "main"); err != nil || id == 0 { if id, err := mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-watchman", built, byBuild("build-2"), "", "main"); err != nil || id == 0 {
t.Fatalf("1146/AC5: a second module from a repository the catalogue builds from was refused (id %d): %v", id, err) t.Fatalf("1146/AC5: a second module from a repository the catalogue builds from was refused (id %d): %v", id, err)
} }
} }