A merge on the operator's Approve into a repository the mesh builds nothing from asks the first build of the modules it holds (issue 1146, AC6, AC7)
This commit is contained in:
@@ -176,6 +176,11 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
|
|||||||
repository, m.Base, m.Commit, already)
|
repository, m.Base, m.Commit, already)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if already == 0 {
|
||||||
|
if done, err := f.firstBuilds(ctx, m, entries, read, now); err != nil || done {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
fmt.Print(nothingReadsIt(repository, m))
|
fmt.Print(nothingReadsIt(repository, m))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// firstBuildWait is how long after the forge made a merge the controller keeps hearing it again for mesh-delivery's word
|
||||||
|
// on it, which may be on its way when the forge's announcement is heard (novox/hq ADR 0432).
|
||||||
|
const firstBuildWait = 2 * time.Minute
|
||||||
|
|
||||||
|
// firstBuilds asks the first build of every module a merge adds to a repository no registered module is built from, when
|
||||||
|
// the merge was announced as made on the operator's Approve of that head (novox/hq ADR 0432, issue 1146): the modules'
|
||||||
|
// directories are the ones the announcement says hold a module.json at the merge commit, as touchedBy reads them. The
|
||||||
|
// merge is kept, so the build's outcome is judged by the rule that registers it (mayRegisterFrom, approvedMove). Without
|
||||||
|
// the Approve nothing is asked, and it says why. Answers whether the merge was dealt with here.
|
||||||
|
func (f following) firstBuilds(ctx context.Context, m link.SourceMoved, entries []inventory.Entry,
|
||||||
|
read map[string][]inventory.ReadRepository, now time.Time) (bool, error) {
|
||||||
|
inv := f.open.inventory
|
||||||
|
repository := m.Owner + "/" + m.Repo
|
||||||
|
added, why, err := firstBuildsOf(ctx, inv, m, entries, read, now)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errFirstNotApproved):
|
||||||
|
fmt.Printf("%s merged into %s (%.8s) adds %s, and no build is asked: %v\n", repository, m.Base, m.Commit,
|
||||||
|
strings.Join(addedDirs(m, entries, read), ", "), err)
|
||||||
|
return true, nil
|
||||||
|
case err != nil:
|
||||||
|
return true, notNow(err)
|
||||||
|
case len(added) == 0:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
|
||||||
|
if err != nil {
|
||||||
|
merged = now
|
||||||
|
}
|
||||||
|
event, err := json.Marshal(keptMerge{SourceMoved: m})
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
if _, err := inv.AddMerge(ctx, inventory.BatchedMerge{Repository: repository, Branch: m.Base, Commit: m.Commit,
|
||||||
|
Merged: merged.UTC(), Heard: now, Event: event}); err != nil {
|
||||||
|
return true, notNow(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so the "+
|
||||||
|
"first build of %s is asked\n", repository, m.Base, m.Commit, why, strings.Join(added, ", "))
|
||||||
|
askBuilds(ctx, inv, m, buildSource{Repository: repository, Seat: gitSeat}, added)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errFirstNotApproved says a merge into a repository the mesh builds nothing from is not one the operator approved.
|
||||||
|
var errFirstNotApproved = errors.New("it is no merge made on the operator's Approve of its head, so a hand `build` " +
|
||||||
|
"registers a module from a repository the mesh does not build yet")
|
||||||
|
|
||||||
|
// addedDirs are the module directories the merge adds to a repository no registered module is built from, none where
|
||||||
|
// the merge is not into the trunk or some module is built from the repository already.
|
||||||
|
func addedDirs(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository) []string {
|
||||||
|
if m.Base != "main" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.Provided && sameRepository(e.Source.Repository, m) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, added, _ := touchedBy(nil, entries, m, read)
|
||||||
|
return added
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstBuildsOf is the directories whose first build a merge asks, and the warrant it was made on; none when the merge
|
||||||
|
// is no first merge into a repository the mesh builds nothing from. errFirstNotApproved when it is, and the operator's
|
||||||
|
// Approve of its head is not on record, a try-again error while mesh-delivery's word may still be on its way.
|
||||||
|
func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved, entries []inventory.Entry,
|
||||||
|
read map[string][]inventory.ReadRepository, now time.Time) ([]string, string, error) {
|
||||||
|
added := addedDirs(m, entries, read)
|
||||||
|
if len(added) == 0 {
|
||||||
|
return nil, "", nil
|
||||||
|
}
|
||||||
|
why, err := approvedAnnounced(ctx, inv, m.Owner, m.Repo, keptMerge{SourceMoved: m})
|
||||||
|
if err == nil {
|
||||||
|
return added, why, nil
|
||||||
|
}
|
||||||
|
merged, perr := time.Parse(time.RFC3339Nano, m.MergedAt)
|
||||||
|
if errors.Is(err, errNotAnnounced) && perr == nil && now.Sub(merged) < firstBuildWait {
|
||||||
|
return nil, "", fmt.Errorf("%w: %w", link.ErrTryAgain, err)
|
||||||
|
}
|
||||||
|
var nope error = fmt.Errorf("%w (%v)", errFirstNotApproved, err)
|
||||||
|
if inventory.Unreachable(err) {
|
||||||
|
nope = err
|
||||||
|
}
|
||||||
|
return nil, "", nope
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1146/AC7: mesh-delivery #1 merged on the operator's Approve into a repository the mesh builds nothing from. The forge's
|
||||||
|
// announcement, heard as the controller hears it (SourceMoved, hearMerge), asks the first build of the module the merge
|
||||||
|
// commit holds; the build's outcome registers it; and the module is in the catalogue. Without the Approve nothing is
|
||||||
|
// asked. Store-backed: skips without one.
|
||||||
|
func TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegistersTheModule(t *testing.T) {
|
||||||
|
const head, commit = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "0177ea7a00000000"
|
||||||
|
for _, approved := range []bool{true, false} {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := open.inventory
|
||||||
|
asked := asksWithPaths(t)
|
||||||
|
|
||||||
|
a := aDeliveryMergeOf(t, head, map[string]string{"novox/mesh-delivery@a1a1a1a1a1a1": head})
|
||||||
|
a.merge.Number = 1
|
||||||
|
wasKept, wasAsk := keptDeliveryMerge, routerAskOf
|
||||||
|
keptDeliveryMerge = func(context.Context, string, string, int, string) (deliveryMerge, bool, error) {
|
||||||
|
return a.merge, approved, nil
|
||||||
|
}
|
||||||
|
routerAskOf = func(context.Context, catalogueReader, string) (*asks.Ask, *asks.Warrant, error) {
|
||||||
|
return &a.ask, &a.warrant, nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { keptDeliveryMerge, routerAskOf = wasKept, wasAsk })
|
||||||
|
|
||||||
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main", HeadSHA: head, Commit: commit, Number: 1,
|
||||||
|
MergedAt: time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano), Paths: []string{"module.json", "main.go"},
|
||||||
|
ModuleDirs: []string{"."}, ModuleDirsSaid: true}
|
||||||
|
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
|
||||||
|
t.Fatalf("approved %v: %v", approved, err)
|
||||||
|
}
|
||||||
|
if !approved {
|
||||||
|
if len(*asked) != 0 {
|
||||||
|
t.Fatalf("1146/AC7: a merge with no Approve asked %v", *asked)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
want := [3]string{"novox/mesh-delivery", "", "main"}
|
||||||
|
if len(*asked) != 1 || (*asked)[0] != want {
|
||||||
|
t.Fatalf("1146/AC7: the merge asked %v, want %v", *asked, want)
|
||||||
|
}
|
||||||
|
build := onTrunk("b-", "novox/mesh-delivery", "git", "", map[string]any{"module": "mesh-delivery-view", "version": "1"})
|
||||||
|
build.Commit = commit
|
||||||
|
if _, _, err := takeIn(ctx, inv, build); err != nil {
|
||||||
|
t.Fatalf("1146/AC7: the outcome of the first build was refused: %v", err)
|
||||||
|
}
|
||||||
|
src, err := inv.SourceOf(ctx, "mesh-delivery-view")
|
||||||
|
if err != nil || src.Repository != "novox/mesh-delivery" {
|
||||||
|
t.Fatalf("1146/AC7: the module is built from %q (%v)", src.Repository, err)
|
||||||
|
}
|
||||||
|
if _, known := inventoryHolds(t, inv, "mesh-delivery-view"); !known {
|
||||||
|
t.Fatal("1146/AC7: the module is not in the catalogue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inventoryHolds(t *testing.T, inv *inventory.Inventory, module string) (inventory.Entry, bool) {
|
||||||
|
t.Helper()
|
||||||
|
entries, err := inv.Catalogued(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Manifest.Module == module {
|
||||||
|
return e, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inventory.Entry{}, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1146/AC6: the first merge into a repository no registered module is built from asks the first build of every module
|
||||||
|
// the merge commit holds when it was made on the operator's Approve of that head, and asks nothing without it.
|
||||||
|
func TestAFirstMergeIntoANewRepositoryAsksItsModulesBuildsOnlyOnTheApprove(t *testing.T) {
|
||||||
|
const head = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
|
||||||
|
now := time.Date(2026, 10, 19, 0, 6, 0, 0, time.UTC)
|
||||||
|
mergedAt := now.Add(-time.Hour).Format(time.RFC3339Nano)
|
||||||
|
heard := link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main", HeadSHA: head, Commit: "0177ea7a",
|
||||||
|
Number: 1, MergedAt: mergedAt, ModuleDirsSaid: true, ModuleDirs: []string{"watchman", "view"},
|
||||||
|
Paths: []string{"view/module.json", "watchman/module.json", "README.md"}}
|
||||||
|
built := inventory.Entry{Source: inventory.Source{Repository: "novox/mesh-catalog", Seat: "git"}}
|
||||||
|
for _, c := range []struct {
|
||||||
|
what string
|
||||||
|
change func(*anApprovedMerge, *link.SourceMoved, *[]inventory.Entry)
|
||||||
|
unkept bool
|
||||||
|
dirs []string
|
||||||
|
refused bool
|
||||||
|
retry bool
|
||||||
|
}{
|
||||||
|
{what: "the Approve of the merged head", dirs: []string{"view", "watchman"}},
|
||||||
|
{what: "a Decline", refused: true, change: func(a *anApprovedMerge, _ *link.SourceMoved, _ *[]inventory.Entry) {
|
||||||
|
a.warrant.Option, a.warrant.Label, a.merge.Option = "decline", "Decline", "decline"
|
||||||
|
}},
|
||||||
|
{what: "no word of mesh-delivery on a merge made long ago", unkept: true, refused: true},
|
||||||
|
{what: "no word yet on a merge just made", unkept: true, retry: true, change: func(_ *anApprovedMerge, m *link.SourceMoved, _ *[]inventory.Entry) {
|
||||||
|
m.MergedAt = now.Add(-time.Second).Format(time.RFC3339Nano)
|
||||||
|
}},
|
||||||
|
{what: "a module already built from the repository", change: func(_ *anApprovedMerge, _ *link.SourceMoved, e *[]inventory.Entry) {
|
||||||
|
*e = append(*e, inventory.Entry{Source: inventory.Source{Repository: "novox/mesh-delivery", Seat: "git"}})
|
||||||
|
}},
|
||||||
|
{what: "a merge into another branch", change: func(_ *anApprovedMerge, m *link.SourceMoved, _ *[]inventory.Entry) { m.Base = "feat" }},
|
||||||
|
{what: "a merge that holds no module.json", change: func(_ *anApprovedMerge, m *link.SourceMoved, _ *[]inventory.Entry) {
|
||||||
|
m.ModuleDirs, m.Paths = nil, []string{"README.md"}
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(c.what, func(t *testing.T) {
|
||||||
|
a := aDeliveryMergeOf(t, head, map[string]string{"novox/mesh-delivery@a1a1a1a1a1a1": head})
|
||||||
|
a.merge.Number = 1
|
||||||
|
m, entries := heard, []inventory.Entry{built}
|
||||||
|
if c.change != nil {
|
||||||
|
c.change(&a, &m, &entries)
|
||||||
|
}
|
||||||
|
wasKept, wasAsk := keptDeliveryMerge, routerAskOf
|
||||||
|
t.Cleanup(func() { keptDeliveryMerge, routerAskOf = wasKept, wasAsk })
|
||||||
|
keptDeliveryMerge = func(context.Context, string, string, int, string) (deliveryMerge, bool, error) {
|
||||||
|
return a.merge, !c.unkept, nil
|
||||||
|
}
|
||||||
|
routerAskOf = func(context.Context, catalogueReader, string) (*asks.Ask, *asks.Warrant, error) {
|
||||||
|
return &a.ask, &a.warrant, nil
|
||||||
|
}
|
||||||
|
dirs, _, err := firstBuildsOf(t.Context(), nil, m, entries, nil, now)
|
||||||
|
switch {
|
||||||
|
case c.retry && !errors.Is(err, link.ErrTryAgain):
|
||||||
|
t.Fatalf("1146/AC6: %s: want a try-again, got %v", c.what, err)
|
||||||
|
case c.refused && !errors.Is(err, errFirstNotApproved):
|
||||||
|
t.Fatalf("1146/AC6: %s: want the Approve missing, got %v (dirs %v)", c.what, err, dirs)
|
||||||
|
case !c.refused && !c.retry && err != nil:
|
||||||
|
t.Fatalf("1146/AC6: %s: %v", c.what, err)
|
||||||
|
case !slices.Equal(dirs, c.dirs):
|
||||||
|
t.Fatalf("1146/AC6: %s: first builds asked of %v, want %v", c.what, dirs, c.dirs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,7 +236,17 @@ func approvedMove(ctx context.Context, inv registrations, forms *sourceForms, bu
|
|||||||
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, r.Commit)
|
||||||
}
|
}
|
||||||
k := announcedOf(merged)
|
return approvedAnnounced(ctx, inv, owner, name, announcedOf(merged))
|
||||||
|
}
|
||||||
|
|
||||||
|
// errNotAnnounced says mesh-delivery's word on a merge is not kept (yet): the merge may have been made by someone else,
|
||||||
|
// or its word may still be on its way.
|
||||||
|
var errNotAnnounced = errors.New("mesh-delivery announced no such merge")
|
||||||
|
|
||||||
|
// approvedAnnounced is the warrant of the merge the forge announced as k: mesh-delivery announced it, and the ask it made
|
||||||
|
// it on holds the operator's Approve of that head. Why it holds, or why not. Shared by the approved move of a known
|
||||||
|
// module and the first build of a new repository's modules (ADR 0420 §7, ADR 0432).
|
||||||
|
func approvedAnnounced(ctx context.Context, inv catalogueReader, owner, name string, k keptMerge) (string, error) {
|
||||||
if k.Number <= 0 || k.HeadSHA == "" {
|
if k.Number <= 0 || k.HeadSHA == "" {
|
||||||
return "", errors.New("the forge's announcement of the merge names no pull request or head")
|
return "", errors.New("the forge's announcement of the merge names no pull request or head")
|
||||||
}
|
}
|
||||||
@@ -245,7 +255,7 @@ func approvedMove(ctx context.Context, inv registrations, forms *sourceForms, bu
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !kept {
|
if !kept {
|
||||||
return "", fmt.Errorf("mesh-delivery announced no merge of %s/%s#%d at %.8s", owner, name, k.Number, k.HeadSHA)
|
return "", fmt.Errorf("%w: %s/%s#%d at %.8s", errNotAnnounced, owner, name, k.Number, k.HeadSHA)
|
||||||
}
|
}
|
||||||
ask, w, err := routerAskOf(ctx, inv, d.AskID)
|
ask, w, err := routerAskOf(ctx, inv, d.AskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -406,7 +406,12 @@ func askNewModules(ctx context.Context, inv *inventory.Inventory, m link.SourceM
|
|||||||
if len(added) == 0 || len(from) == 0 {
|
if len(added) == 0 || len(from) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
source := buildSource{Repository: from[0].Source.Repository, Seat: from[0].Source.Seat}
|
return askBuilds(ctx, inv, m, buildSource{Repository: from[0].Source.Repository, Seat: from[0].Source.Seat}, added)
|
||||||
|
}
|
||||||
|
|
||||||
|
// askBuilds asks the first build of each directory of a merge's repository, as askNewModules does, from the source given.
|
||||||
|
func askBuilds(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, source buildSource,
|
||||||
|
added []string) []string {
|
||||||
var asked []string
|
var asked []string
|
||||||
for _, dir := range added {
|
for _, dir := range added {
|
||||||
path := dir
|
path := dir
|
||||||
|
|||||||
Vendored
+1
@@ -164,3 +164,4 @@
|
|||||||
./internal/inventory TestWhetherAMachineReadsModulesIsItsLatestWord
|
./internal/inventory TestWhetherAMachineReadsModulesIsItsLatestWord
|
||||||
./cmd/mesh-controller TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem
|
./cmd/mesh-controller TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem
|
||||||
./cmd/mesh-controller TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToTheOthers
|
./cmd/mesh-controller TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToTheOthers
|
||||||
|
./cmd/mesh-controller TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegistersTheModule
|
||||||
|
|||||||
Reference in New Issue
Block a user