diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go index b3aabfa..79b4345 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go @@ -20,6 +20,8 @@ const ( getFileTool = "gitea.gitea_get_file" compareTool = "gitea.gitea_compare" commitsTool = "gitea.gitea_commits" + // gitea_list_branches answers {branches: [{name, commit}]}, every branch with the commit it points at. + branchesTool = "gitea.gitea_list_branches" ) // notFoundStatus is the 404 of the forge's error, `Gitea API : `. @@ -102,3 +104,26 @@ func (f forgeReads) FullSHA(_ context.Context, repository, commit string) (strin } return a.Commits[0].SHA, nil } + +// BranchHeads is the commit each branch of the repository points at. +func (f forgeReads) BranchHeads(_ context.Context, repository string) (map[string]string, error) { + owner, repo := ownerRepo(repository) + raw, err := f.ask(branchesTool, map[string]string{"owner": owner, "repo": repo}) + if err != nil { + return nil, err + } + var a struct { + Branches *[]struct { + Name string `json:"name"` + Commit string `json:"commit"` + } `json:"branches"` + } + if err := json.Unmarshal(raw, &a); err != nil || a.Branches == nil { + return nil, fmt.Errorf("the answer of %s is not {branches: [{name, commit}]}: %v", branchesTool, err) + } + heads := map[string]string{} + for _, b := range *a.Branches { + heads[b.Name] = b.Commit + } + return heads, nil +} diff --git a/modules/mesh-delivery-checker/internal/check/case.go b/modules/mesh-delivery-checker/internal/check/case.go index 22850d4..1c99029 100644 --- a/modules/mesh-delivery-checker/internal/check/case.go +++ b/modules/mesh-delivery-checker/internal/check/case.go @@ -3,6 +3,7 @@ package check import ( "fmt" "maps" + "slices" "sort" "strings" "sync" @@ -40,6 +41,9 @@ type Case struct { Queued time.Time // Try narrows a try's check (its level, its tests); nil for a pull request's. Try *TryOptions + + notesMu sync.Mutex + notes []string // Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at. Progress *Progress // Kind is what asked the check (KindNew, KindNodes, KindTrunk): a re-judge after a trunk move is asked on the build @@ -148,3 +152,19 @@ func (t *Trunks) Heads() map[string]string { defer t.mu.Unlock() return maps.Clone(t.heads) } + +// note puts a word of the check's way on the case, for a try's line (issue 1921). +func (c *Case) note(s string) { + c.notesMu.Lock() + defer c.notesMu.Unlock() + if !slices.Contains(c.notes, s) { + c.notes = append(c.notes, s) + } +} + +// Notes are the words noted on the case, in the order they were noted. +func (c *Case) Notes() []string { + c.notesMu.Lock() + defer c.notesMu.Unlock() + return slices.Clone(c.notes) +} diff --git a/modules/mesh-delivery-checker/internal/check/pins.go b/modules/mesh-delivery-checker/internal/check/pins.go index c07a8c4..1de0a6e 100644 --- a/modules/mesh-delivery-checker/internal/check/pins.go +++ b/modules/mesh-delivery-checker/internal/check/pins.go @@ -6,6 +6,7 @@ import ( "fmt" "path" "regexp" + "sort" "strings" ) @@ -36,6 +37,8 @@ type Forge interface { OnTrunk(ctx context.Context, repository, commit string) (bool, error) // FullSHA is the 40-digit sha of a commit named by its first 12 digits. FullSHA(ctx context.Context, repository, commit string) (string, error) + // BranchHeads is the commit every branch of a repository points at, by branch name (issue 1921). + BranchHeads(ctx context.Context, repository string) (map[string]string, error) } var pseudoVersion = regexp.MustCompile(`-([0-9a-f]{12})$`) @@ -93,6 +96,18 @@ func (st *state) checkPinsOf(ctx context.Context, t *target) verdictPtr { if onTrunk { continue } + if st.c.Try != nil { + // A try only (issue 1921): a repository tried on a branch that mesh-sdk shares by name (a milestone's branch across + // repositories) is built with that branch's mesh-sdk head, which no group pull request holds before the pull requests exist. + branch, err := st.sameBranchPin(ctx, t, pin) + if err != nil { + return failed(forgeMachine(err, t.node), fmt.Sprintf("%s#%d: whether mesh-sdk %s is the head of a branch cannot be told: %v%s", t.pull.Repository, t.pull.Number, pin, err, unheardWords(err))) + } + if branch != "" { + st.c.note(fmt.Sprintf("pin accepted: head of mesh-sdk %s", branch)) + continue + } + } nor := "the group has no mesh-sdk pull request" if sdk != nil { nor = "the head of the group's mesh-sdk pull request is " + short12(sdk.Head) @@ -103,6 +118,35 @@ func (st *state) checkPinsOf(ctx context.Context, t *target) verdictPtr { return nil } +// sameBranchPin names the branch of the tried repository that points at the tried head and that mesh-sdk also has, at the +// commit the pin names; empty when there is none. +func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (string, error) { + mine, err := st.j.Forge.BranchHeads(ctx, t.pull.Repository) + if err != nil { + return "", err + } + var same []string + for name, head := range mine { + if head == t.pull.Head && name != "main" { + same = append(same, name) + } + } + if len(same) == 0 { + return "", nil + } + sdk, err := st.j.Forge.BranchHeads(ctx, SDKRepository) + if err != nil { + return "", err + } + sort.Strings(same) + for _, name := range same { + if h, ok := sdk[name]; ok && strings.HasPrefix(h, pin) { + return name, nil + } + } + return "", nil +} + // failureVerdict is a verdict of failure the check ends with. // pinHint is added to the failure of a Go member's fetch that names mesh-sdk while the group holds a mesh-sdk pull request: diff --git a/modules/mesh-delivery-checker/internal/check/pins_test.go b/modules/mesh-delivery-checker/internal/check/pins_test.go index d1083be..0c4dba0 100644 --- a/modules/mesh-delivery-checker/internal/check/pins_test.go +++ b/modules/mesh-delivery-checker/internal/check/pins_test.go @@ -18,6 +18,15 @@ type fakeForge struct { onTrunk map[string]bool // by commit prefix err error errAt map[string]error // by ref: a read at that ref fails with it + // branches are the branches of a repository with the commit each points at. + branches map[string]map[string]string +} + +func (f *fakeForge) BranchHeads(_ context.Context, repository string) (map[string]string, error) { + if f.err != nil { + return nil, f.err + } + return f.branches[repository], nil } func (f *fakeForge) File(_ context.Context, repository, path, ref string) (string, error) { @@ -358,3 +367,44 @@ func TestTheNodeEnginesCheckFailsWhereTheControllersTrunkIsNotKnown(t *testing.T t.Fatalf("verdict = %+v", v) } } + +// sameBranchBench is a try of mesh-delivery's feat/r6 at head1 whose go.mod pins the SDK at pin, with the SDK's feat/r6 at head2. +func sameBranchBench(t *testing.T, pin string) (*bench, *Case) { + b := groupBench(t, pin) + b.ex.Forge.(*fakeForge).branches = map[string]map[string]string{ + repoA: {"main": trunkA, "feat/r6": head1}, + repoSDK: {"main": "cb54756c3865" + strings.Repeat("0", 28), "feat/r6": head2}, + } + c := caseOf(t, pullOf(repoA, 7, head1)) + c.Try = &TryOptions{} + return b, c +} + +func TestATryPinnedToTheHeadOfTheSDKBranchOfTheTriedBranchPasses(t *testing.T) { // 1921/AC1 + b, c := sameBranchBench(t, head2[:12]) + if v := b.judge(t, c).Verdict; v.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", v) + } + if n := c.Notes(); len(n) != 1 || n[0] != "pin accepted: head of mesh-sdk feat/r6" { + t.Errorf("notes = %q", n) + } +} + +func TestATryPinnedToAnotherCommitIsStillRefused(t *testing.T) { // 1921/AC2 + b, c := sameBranchBench(t, "9aaa5c80ac5c") + v := b.judge(t, c).Verdict + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "9aaa5c80ac5c") || !strings.Contains(v.Failed.Words, "neither on mesh-sdk's trunk nor the group's head") { + t.Fatalf("verdict = %+v", v) + } + if len(c.Notes()) != 0 { + t.Errorf("notes = %q", c.Notes()) + } +} + +func TestAMergeCheckWithTheSameBranchPinIsStillRefusedWithoutAGroupPullRequest(t *testing.T) { // 1921/AC3 + b, _ := sameBranchBench(t, head2[:12]) + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict // no Try: a pull request's check + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "the group has no mesh-sdk pull request") { + t.Fatalf("verdict = %+v", v) + } +} diff --git a/modules/mesh-delivery-checker/internal/check/try.go b/modules/mesh-delivery-checker/internal/check/try.go index 812773b..5f0be8b 100644 --- a/modules/mesh-delivery-checker/internal/check/try.go +++ b/modules/mesh-delivery-checker/internal/check/try.go @@ -329,6 +329,9 @@ func (tr *Tryer) run(ctx context.Context, key string) { t.Took = t.Ended.Sub(t.Started).Round(time.Second).String() t.Levels, t.Machines = v.Levels, v.Machines t.Said = Report(v, cs, cs.Pulls[0], out.Builds, out.Slowest, out.Judged) + for _, n := range cs.Notes() { + t.Said += " | " + n + } switch { case !judged || IsNodesFailure(v): t.State = TryError diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index c6af543..8df57af 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -103,7 +103,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 } // 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no // build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4). - if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want only the forge's reads for a mesh-sdk pin (ADR 0484 clause 9)", m.Invokes) } if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index 67c97f2..c53c7b5 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -47,7 +47,8 @@ "invokes": [ "gitea.gitea_commits", "gitea.gitea_compare", - "gitea.gitea_get_file" + "gitea.gitea_get_file", + "gitea.gitea_list_branches" ], "own-secrets": {