Let the module graph decide what a pull request's check runs, in two layers (hq ADR 0237)
Every pull request the forge announces is mapped onto the mesh's module graph by the merge handler's rule (issue 278): touching a module — or adding one — runs the gate (mesh/merge-gate), its judge chosen by the graph (the controller judges itself, the node-engine by its validator); a repository of the mesh that touches none runs only its own merge-check.sh (mesh/repo-check), a warning when it has none. Nothing is left pending: a repository outside the mesh touching nothing is told so as a pass. The gate moves out of the per-repository scripts into the build seat, so a script is the repository's own tests and declares its toolchain (go or typescript). The controller's manifest names every verb of its seat again (ADR 0132), held by a test.
This commit is contained in:
@@ -249,7 +249,7 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
||||
v, err = builder.Check(building, builder.Command, checkSpecOf(request), workspace, registry, forgeFrom(), say)
|
||||
if err == nil {
|
||||
result.Check = &link.CheckOutcome{Verdict: v.Verdict, Summary: v.Summary, Report: v.Report,
|
||||
Took: v.Took.Round(time.Second).String()}
|
||||
Took: v.Took.Round(time.Second).String(), Gate: layerOf(v.Gate), RepoCheck: layerOf(v.Repo)}
|
||||
}
|
||||
} else if err == nil {
|
||||
// The package-registry credential is a build input, so it is resolved before the clone: a
|
||||
@@ -334,13 +334,22 @@ func checkSpecOf(request link.BuildRequest) builder.CheckSpec {
|
||||
c := request.Check
|
||||
spec := builder.CheckSpec{ID: request.ID, Repository: request.Repository, Ref: request.Ref,
|
||||
Owner: c.Owner, Repo: c.Repo, Number: c.Number, Paths: c.Paths, Beside: map[string]builder.Beside{},
|
||||
Toolchain: builder.ToolchainOf(request.Held)}
|
||||
Modules: c.Modules, New: c.New, Manifests: c.Manifests, Judge: c.Judge,
|
||||
Toolchain: builder.ToolchainOf(request.Held), Toolchains: builder.ToolchainsOf(request.Held)}
|
||||
for dir, b := range c.Beside {
|
||||
spec.Beside[dir] = builder.Beside{Repository: b.Repository, Ref: b.Ref}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
// layerOf is one layer of a check as the outcome carries it.
|
||||
func layerOf(l *builder.Layer) *link.CheckLayer {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &link.CheckLayer{Verdict: l.Verdict, Summary: l.Summary, Modules: l.Modules}
|
||||
}
|
||||
|
||||
// packagesFrom is where a build resolves the mesh's own published packages — the SDK above all
|
||||
// (novox/hq ADR 0076, issue 053).
|
||||
//
|
||||
|
||||
@@ -92,3 +92,14 @@ func TestRegistrationRefusesADefinitionNamingAnInstallation(t *testing.T) {
|
||||
t.Fatalf("a name declared on purpose passes; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// **The controller's own manifest names every verb of its seat** (novox/hq ADR 0132): its tools lagged
|
||||
// the seat's verbs for weeks, and `module check` — the gate's first step for a change touching it —
|
||||
// refused it. Held here, so a verb added to the table without the manifest fails this repository's
|
||||
// own suite rather than its next pull request's gate.
|
||||
func TestTheControllersManifestServesEveryVerbOfItsSeat(t *testing.T) {
|
||||
var out strings.Builder
|
||||
if err := moduleCheck([]string{"../../module.json"}, &out); err != nil {
|
||||
t.Fatalf("the controller's own module.json fails module check: %v\n%s", err, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
+232
-35
@@ -4,6 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -12,43 +15,189 @@ import (
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// A pull request's merge check (novox/hq to-be 45 §9): the forge announces a pull request's new head,
|
||||
// the controller asks the build seat to check it, and says the verdict as `checked`, which the forge's
|
||||
// holder sets as the pull request's status. **Before merge, never after**: every check the mesh had ran
|
||||
// after a merge, on a machine.
|
||||
// A pull request's merge check (novox/hq to-be 45 §9, ADR 0237 as amended 2026-10-06): the forge
|
||||
// announces every pull request's new head, the controller decides what is checked, asks the build seat to
|
||||
// check it, and says the verdict as `checked`, which the forge's holder sets as the pull request's
|
||||
// statuses. **Before merge, never after**: every check the mesh had ran after a merge, on a machine.
|
||||
//
|
||||
// What is checked is decided here and run there. Here: whether the mesh builds anything from the
|
||||
// repository into that branch — a repository it builds nothing from is not its to judge — and what the
|
||||
// check reads beside it: the controller the mesh runs (its judge, for a catalogue change: a manifest
|
||||
// that controller cannot read fails, which is version skew caught), the catalogue the mesh holds, the
|
||||
// host it runs. There: the repository's own merge-check.sh, or the merge gate alone for a repository
|
||||
// that declares none (internal/builder/check.go).
|
||||
// **The mesh's module graph decides, not the repository.** The controller holds the graph — every module,
|
||||
// the repository and directory it is built from — and maps the pull request's changed paths onto it by
|
||||
// the rule the merge handler uses (issue 278): a changed file inside a directory holding a module.json at
|
||||
// the head is that module's, held or not; a file in no such directory, in a repository modules are built
|
||||
// from into that branch, is shared code, and touches every module built from it there. A directory the
|
||||
// change adds a module in, which the graph does not hold yet, is a new module and is checked too.
|
||||
//
|
||||
// - touches a module: the build seat runs **the gate** — `mesh/merge-gate`, the touched manifests, every
|
||||
// machine composed with the change, the replays — and the repository's own merge-check.sh beside it;
|
||||
// - touches none, in a repository that is the mesh's (it sources a module on some branch, or shares the
|
||||
// core's owner): the gate is a pass that says so — a fact, not a missing check — and the repository's
|
||||
// own merge-check.sh runs as `mesh/repo-check`, a warning when it has none;
|
||||
// - touches none, anywhere else: the gate is a pass that says so, and nothing more is said.
|
||||
//
|
||||
// What is checked is decided here and run there (internal/builder/check.go).
|
||||
|
||||
// checkTimeout is how long one check may run on the build seat. Said here so the ask's watchdog (S6)
|
||||
// and the builder agree on what late means.
|
||||
const checkTimeout = 45 * time.Minute
|
||||
|
||||
// PullUpdated asks for a pull request's merge check.
|
||||
// noModuleTouched is the gate's word for a change that touches nothing of the graph.
|
||||
const noModuleTouched = "the change touches no module of the mesh's graph"
|
||||
|
||||
// noMergeCheck is the repository layer's word for a repository of the mesh with no merge-check.sh.
|
||||
const noMergeCheck = "the repository declares no merge-check.sh: none of its own tests run before it merges"
|
||||
|
||||
// coreModules are the modules whose repositories are the mesh's core, by the directory a check finds
|
||||
// each beside it — and whose owner is the mesh's own.
|
||||
var coreModules = map[string]string{"mesh-controller": "mesh-controller", "mesh-host": "mesh-host",
|
||||
"node-tools": "mesh-tools", "nats": "mesh-catalog"}
|
||||
|
||||
// checkScope is what a pull request touches of the mesh's graph.
|
||||
type checkScope struct {
|
||||
// Modules are the graph's modules built from the repository into the pull request's base that the
|
||||
// change touches, sorted; New the directories it adds a module in that the graph does not hold.
|
||||
Modules []string
|
||||
New []string
|
||||
// Manifests are their manifests in the change's tree.
|
||||
Manifests []string
|
||||
// Mesh says the repository is the mesh's: modules are built from it on some branch, its owner is the
|
||||
// core's, or the change adds a module to it.
|
||||
Mesh bool
|
||||
// Judge is who judges the gate (link.JudgeSelf, link.JudgeValidator, or the running controller).
|
||||
Judge string
|
||||
// From is a module built from the repository, for how the mesh clones it; nil when none is.
|
||||
From *inventory.Entry
|
||||
}
|
||||
|
||||
func (s checkScope) gated() bool { return len(s.Modules)+len(s.New) > 0 }
|
||||
|
||||
// pullScope maps a pull request onto the mesh's module graph.
|
||||
func pullScope(p link.PullUpdated, entries []inventory.Entry, read map[string][]inventory.ReadRepository) checkScope {
|
||||
m := link.SourceMoved{Owner: p.Owner, Repo: p.Repo, Base: p.Base, CloneURL: p.CloneURL, Commit: p.Commit,
|
||||
Paths: p.Paths, PathsTruncated: p.PathsTruncated, ModuleDirs: p.ModuleDirs, ModuleDirsSaid: p.ModuleDirsSaid}
|
||||
var s checkScope
|
||||
var from []inventory.Entry
|
||||
known := map[string]bool{}
|
||||
owners := map[string]bool{}
|
||||
for i, e := range entries {
|
||||
if e.Provided {
|
||||
continue
|
||||
}
|
||||
if _, core := coreModules[e.Manifest.Module]; core {
|
||||
if owner := sourceOwner(e.Source.Repository); owner != "" {
|
||||
owners[owner] = true
|
||||
}
|
||||
}
|
||||
if !sameRepository(e.Source.Repository, m) {
|
||||
continue
|
||||
}
|
||||
s.Mesh = true
|
||||
known[strings.Trim(e.Source.Path, "/")] = true
|
||||
if s.From == nil {
|
||||
s.From = &entries[i]
|
||||
}
|
||||
if sourceIs(e.Source, m) {
|
||||
from = append(from, e)
|
||||
}
|
||||
}
|
||||
touched := map[string]inventory.Entry{}
|
||||
if len(from) > 0 {
|
||||
for _, e := range whatTheMergeTouched(from, entries, m) {
|
||||
touched[e.Manifest.Module] = e
|
||||
}
|
||||
}
|
||||
// A module whose build packages source from this repository's branch is touched by any change to it.
|
||||
for _, e := range entries {
|
||||
if !e.Provided && readsFrom(read[e.Manifest.Module], m) {
|
||||
touched[e.Manifest.Module] = e
|
||||
}
|
||||
}
|
||||
for name, e := range touched {
|
||||
s.Modules = append(s.Modules, name)
|
||||
if sameRepository(e.Source.Repository, m) {
|
||||
s.Manifests = append(s.Manifests, path.Join(strings.Trim(e.Source.Path, "/"), "module.json"))
|
||||
}
|
||||
switch name {
|
||||
case "mesh-controller":
|
||||
s.Judge = link.JudgeSelf
|
||||
case "mesh-host":
|
||||
if s.Judge == "" {
|
||||
s.Judge = link.JudgeValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
// A module the graph does not hold yet, in a directory the head says holds one — or at the root.
|
||||
for _, d := range saidModuleDirs(m) {
|
||||
if !known[d] {
|
||||
s.New = append(s.New, d)
|
||||
s.Manifests = append(s.Manifests, d+"/module.json")
|
||||
}
|
||||
}
|
||||
// And, said or not, a manifest the change adds or moves in a directory the graph does not know.
|
||||
for _, changed := range p.Paths {
|
||||
changed = strings.Trim(changed, "/")
|
||||
if path.Base(changed) != "module.json" {
|
||||
continue
|
||||
}
|
||||
d := path.Dir(changed)
|
||||
if d == "." {
|
||||
d = ""
|
||||
}
|
||||
if !known[d] {
|
||||
if d == "" {
|
||||
d = "."
|
||||
}
|
||||
s.New = append(s.New, d)
|
||||
s.Manifests = append(s.Manifests, changed)
|
||||
}
|
||||
}
|
||||
sort.Strings(s.Modules)
|
||||
sort.Strings(s.New)
|
||||
s.New = slices.Compact(s.New)
|
||||
sort.Strings(s.Manifests)
|
||||
s.Manifests = slices.Compact(s.Manifests)
|
||||
if owners[strings.ToLower(p.Owner)] || s.gated() {
|
||||
s.Mesh = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// sourceOwner is the owner of a recorded repository, a path on the git seat or a URL: novox/mesh-host → novox.
|
||||
func sourceOwner(repository string) string {
|
||||
parts := strings.Split(strings.Trim(strings.TrimSuffix(repository, ".git"), "/"), "/")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(parts[len(parts)-2])
|
||||
}
|
||||
|
||||
// PullUpdated decides a pull request's merge check, and asks for it when there is something to run.
|
||||
func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error {
|
||||
inv := f.open.inventory
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
moved := link.SourceMoved{Owner: p.Owner, Repo: p.Repo, Base: p.Base, CloneURL: p.CloneURL}
|
||||
var from *inventory.Entry
|
||||
for i, e := range entries {
|
||||
if !e.Provided && sourceIs(e.Source, moved) {
|
||||
from = &entries[i]
|
||||
break
|
||||
}
|
||||
read, err := inv.ReadRepositories(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if from == nil {
|
||||
fmt.Printf("%s/%s#%d (%.8s): the mesh builds nothing from it into %s, so it is not the mesh's to check\n",
|
||||
p.Owner, p.Repo, p.Number, p.Commit, p.Base)
|
||||
scope := pullScope(p, entries, read)
|
||||
direct := link.Checked{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Commit: p.Commit,
|
||||
ID: link.NewBuildID(time.Now()), Verdict: "pass", Summary: noModuleTouched,
|
||||
Gate: &link.CheckLayer{Verdict: "pass", Summary: noModuleTouched}}
|
||||
switch {
|
||||
case !scope.gated() && !scope.Mesh:
|
||||
fmt.Printf("%s/%s#%d (%.8s): %s, and the repository is not the mesh's: said, nothing run\n",
|
||||
p.Owner, p.Repo, p.Number, p.Commit, noModuleTouched)
|
||||
sayChecked(ctx, direct)
|
||||
return nil
|
||||
case !scope.gated() && p.MergeCheckSaid && !p.MergeCheck:
|
||||
direct.RepoCheck = &link.CheckLayer{Verdict: "warning", Summary: noMergeCheck}
|
||||
fmt.Printf("%s/%s#%d (%.8s): %s; %s\n", p.Owner, p.Repo, p.Number, p.Commit, noModuleTouched, noMergeCheck)
|
||||
sayChecked(ctx, direct)
|
||||
return nil
|
||||
}
|
||||
request, err := checkRequestFor(ctx, f.open, p, *from, entries)
|
||||
request, err := checkRequestFor(ctx, f.open, p, scope, entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,14 +210,18 @@ func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error {
|
||||
if err := ask.Ask(ctx, request); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s/%s#%d (%.8s): asked %s to check it before it merges, as %s\n", p.Owner, p.Repo, p.Number,
|
||||
p.Commit, seat, request.ID)
|
||||
what := "its own merge-check.sh alone: " + noModuleTouched
|
||||
if scope.gated() {
|
||||
what = "the gate over " + strings.Join(append(append([]string{}, scope.Modules...), scope.New...), ", ")
|
||||
}
|
||||
fmt.Printf("%s/%s#%d (%.8s): asked %s to check it before it merges — %s — as %s\n", p.Owner, p.Repo, p.Number,
|
||||
p.Commit, seat, what, request.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRequestFor is the ask for one pull request's head: the repository as the mesh clones it, the head,
|
||||
// and what is read beside it.
|
||||
func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, from inventory.Entry,
|
||||
func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scope checkScope,
|
||||
entries []inventory.Entry) (link.BuildRequest, error) {
|
||||
shelf := map[string]catalogue.Manifest{}
|
||||
for _, e := range entries {
|
||||
@@ -84,7 +237,12 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, from
|
||||
}
|
||||
return clonedFromSeat(world, s.Seat, s.Repository)
|
||||
}
|
||||
repository, err := clone(from.Source)
|
||||
// As the mesh clones a module built from it; a repository no module is built from, from the forge.
|
||||
source := inventory.Source{Seat: gitSeat, Repository: p.Owner + "/" + p.Repo}
|
||||
if scope.From != nil {
|
||||
source = scope.From.Source
|
||||
}
|
||||
repository, err := clone(source)
|
||||
if err != nil {
|
||||
return link.BuildRequest{}, err
|
||||
}
|
||||
@@ -94,10 +252,8 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, from
|
||||
}
|
||||
// Beside it, at what the mesh runs: each core repository by the module the mesh builds from it.
|
||||
beside := map[string]link.CheckedOut{}
|
||||
byModule := map[string]string{"mesh-controller": "mesh-controller", "mesh-host": "mesh-host",
|
||||
"node-tools": "mesh-tools", "nats": "mesh-catalog"}
|
||||
for _, e := range entries {
|
||||
dir, core := byModule[e.Manifest.Module]
|
||||
dir, core := coreModules[e.Manifest.Module]
|
||||
if !core || e.Provided || e.Source.Repository == "" {
|
||||
continue
|
||||
}
|
||||
@@ -130,9 +286,10 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, from
|
||||
Ref: p.Commit,
|
||||
Held: heldBy(ctx),
|
||||
Seats: seatBases(ctx),
|
||||
Source: sourceOnSeat(from.Source),
|
||||
Source: sourceOnSeat(source),
|
||||
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base,
|
||||
Paths: p.Paths, Beside: beside},
|
||||
Paths: p.Paths, Beside: beside, Modules: scope.Modules, New: scope.New, Manifests: scope.Manifests,
|
||||
Judge: scope.Judge},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -162,6 +319,12 @@ const maxCheckReport = 60 << 10
|
||||
// checked says a merge check's verdict as the controller's `checked`. Nothing is recorded or
|
||||
// registered: a check builds nothing (issue 240's rule for a dry run, kept for a check).
|
||||
func checked(ctx context.Context, result link.BuildResult) {
|
||||
sayChecked(ctx, checkedOf(result))
|
||||
}
|
||||
|
||||
// checkedOf is what a check's outcome says: each layer, and an error — never a pass — for a check that
|
||||
// could not run.
|
||||
func checkedOf(result link.BuildResult) link.Checked {
|
||||
c := link.Checked{ID: result.ID, On: result.On, Commit: result.Ref}
|
||||
if result.Checked != nil {
|
||||
c.Owner, c.Repo, c.Number = result.Checked.Owner, result.Checked.Repo, result.Checked.Number
|
||||
@@ -169,8 +332,13 @@ func checked(ctx context.Context, result link.BuildResult) {
|
||||
switch {
|
||||
case result.Check != nil:
|
||||
c.Verdict, c.Summary, c.Report = result.Check.Verdict, result.Check.Summary, result.Check.Report
|
||||
c.Gate, c.RepoCheck = result.Check.Gate, result.Check.RepoCheck
|
||||
if c.Gate == nil {
|
||||
// A build seat from before the layers: its verdict is the gate's.
|
||||
c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary}
|
||||
}
|
||||
case result.Failed != "":
|
||||
// The check could not run: an error, never read as a pass.
|
||||
// The check could not run: an error, never read as a pass — on both layers it was asked for.
|
||||
c.Verdict, c.Summary = "error", "the check could not run: "+firstLine(result.Failed)
|
||||
default:
|
||||
c.Verdict, c.Summary = "error", "the build seat answered the check with no verdict"
|
||||
@@ -178,11 +346,40 @@ func checked(ctx context.Context, result link.BuildResult) {
|
||||
if c.Verdict == "" {
|
||||
c.Verdict = "error"
|
||||
}
|
||||
if result.Check == nil {
|
||||
c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary}
|
||||
c.RepoCheck = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary}
|
||||
}
|
||||
if result.Checked != nil && c.Gate != nil && len(c.Gate.Modules) == 0 {
|
||||
c.Gate.Modules = append(append([]string{}, result.Checked.Modules...), prefixedAll("new:", result.Checked.New)...)
|
||||
}
|
||||
for _, l := range []*link.CheckLayer{c.Gate, c.RepoCheck} {
|
||||
if l != nil && l.Verdict == "" {
|
||||
l.Verdict = "error"
|
||||
}
|
||||
}
|
||||
if len(c.Report) > maxCheckReport {
|
||||
c.Report = "…" + c.Report[len(c.Report)-maxCheckReport:]
|
||||
}
|
||||
fmt.Printf("%s: %s/%s#%d at %.8s checked on %s: %s — %s\n", result.ID, c.Owner, c.Repo, c.Number, c.Commit,
|
||||
orSomewhere(c.On), strings.ToUpper(c.Verdict), c.Summary)
|
||||
return c
|
||||
}
|
||||
|
||||
func prefixedAll(prefix string, items []string) []string {
|
||||
out := make([]string, 0, len(items))
|
||||
for _, i := range items {
|
||||
out = append(out, prefix+i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sayChecked says a merge check's verdict on the bus, where the forge's holder hears it.
|
||||
func sayChecked(ctx context.Context, c link.Checked) {
|
||||
repo := "none"
|
||||
if c.RepoCheck != nil {
|
||||
repo = strings.ToUpper(c.RepoCheck.Verdict) + " — " + c.RepoCheck.Summary
|
||||
}
|
||||
fmt.Printf("%s: %s/%s#%d at %.8s checked on %s: gate %s — %s; repository %s\n", c.ID, c.Owner, c.Repo, c.Number,
|
||||
c.Commit, orSomewhere(c.On), strings.ToUpper(c.Verdict), c.Summary, repo)
|
||||
if checkEvents == nil {
|
||||
return
|
||||
}
|
||||
@@ -193,6 +390,6 @@ func checked(ctx context.Context, result link.BuildResult) {
|
||||
stating, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
defer stop()
|
||||
if err := checkEvents.PublishSeatEvent(stating, link.MeshControllerSeat, link.KeyChecked, body); err != nil {
|
||||
fmt.Printf("%s: the verdict could not be said, so the pull request is not told it: %v\n", result.ID, err)
|
||||
fmt.Printf("%s: the verdict could not be said, so the pull request is not told it: %v\n", c.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// **The mesh's module graph decides what a pull request's check runs**, not the repository (novox/hq
|
||||
// ADR 0237 as amended): a change is mapped onto the graph by the rule a merge is (issue 278), the gate
|
||||
// runs when it touches a module — a new one included — and a repository that is the mesh's and touches
|
||||
// none still has its own merge-check.sh run.
|
||||
func TestThePullRequestIsMappedOntoTheModuleGraph(t *testing.T) {
|
||||
const catalogue = "http://forge.internal:20000/novox/mesh-catalog.git"
|
||||
const controller = "http://forge.internal:20000/novox/mesh-controller.git"
|
||||
const host = "http://forge.internal:20000/novox/mesh-host.git"
|
||||
photos := fromRepo("photos", "http://forge.internal:20000/novox/photos.git", "")
|
||||
photos.Source.Ref = "nox-mesh"
|
||||
snake := fromRepo("snake", "jschoubben/snake", "")
|
||||
snake.Source.Seat = "git"
|
||||
entries := []inventory.Entry{
|
||||
fromRepo("gitea", catalogue, "modules/gitea"),
|
||||
fromRepo("keycloak", catalogue, "modules/keycloak"),
|
||||
fromRepo("nats", catalogue, "modules/nats"),
|
||||
fromRepo("mesh-controller", controller, ""),
|
||||
fromRepo("mesh-host", host, ""),
|
||||
photos, snake,
|
||||
}
|
||||
pull := func(owner, repo, base string, paths []string, dirs ...string) link.PullUpdated {
|
||||
return link.PullUpdated{Owner: owner, Repo: repo, Base: base, Commit: "abc", Paths: paths,
|
||||
ModuleDirs: dirs, ModuleDirsSaid: true}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
p link.PullUpdated
|
||||
modules, new, manifest string
|
||||
mesh bool
|
||||
judge string
|
||||
}{
|
||||
{"one module's own files", pull("novox", "mesh-catalog", "main", []string{"modules/gitea/index.ts"}, "modules/gitea"),
|
||||
"gitea", "", "modules/gitea/module.json", true, ""},
|
||||
{"shared code touches every module built from the repository",
|
||||
pull("novox", "mesh-catalog", "main", []string{"tsconfig.json"}), "gitea,keycloak,nats", "",
|
||||
"modules/gitea/module.json,modules/keycloak/module.json,modules/nats/module.json", true, ""},
|
||||
{"a new module, said by the head", pull("novox", "mesh-catalog", "main",
|
||||
[]string{"modules/newmod/index.ts", "modules/newmod/module.json"}, "modules/newmod"),
|
||||
"", "modules/newmod", "modules/newmod/module.json", true, ""},
|
||||
{"the controller judges itself", pull("novox", "mesh-controller", "main", []string{"cmd/x.go"}),
|
||||
"mesh-controller", "", "module.json", true, "self"},
|
||||
{"the node-engine is judged with its validator", pull("novox", "mesh-host", "main", []string{"validate/v.go"}),
|
||||
"mesh-host", "", "module.json", true, "validator"},
|
||||
{"the core's owner, no module: the mesh's, its own check alone", pull("novox", "hq", "main", []string{"README.md"}),
|
||||
"", "", "", true, ""},
|
||||
{"a branch nothing is built from: the mesh's repository, no module", pull("novox", "photos", "master",
|
||||
[]string{"server/x.js"}), "", "", "", true, ""},
|
||||
{"the branch a module is built from", pull("novox", "photos", "nox-mesh", []string{"server/x.js"}),
|
||||
"photos", "", "module.json", true, ""},
|
||||
{"a repository on the forge's seat", pull("jschoubben", "snake", "main", []string{"index.html"}),
|
||||
"snake", "", "module.json", true, ""},
|
||||
{"a repository of nobody's, touching nothing", pull("someone", "dotfiles", "main", []string{"x"}),
|
||||
"", "", "", false, ""},
|
||||
{"a repository adding a module at its root", pull("someone", "newapp", "main", []string{"module.json", "x.js"}),
|
||||
"", ".", "module.json", true, ""},
|
||||
} {
|
||||
s := pullScope(c.p, entries, nil)
|
||||
got := []string{strings.Join(s.Modules, ","), strings.Join(s.New, ","), strings.Join(s.Manifests, ",")}
|
||||
want := []string{c.modules, c.new, c.manifest}
|
||||
for i, what := range []string{"modules", "new", "manifests"} {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("%s: %s %q, wanted %q", c.what, what, got[i], want[i])
|
||||
}
|
||||
}
|
||||
if s.Mesh != c.mesh || s.Judge != c.judge {
|
||||
t.Errorf("%s: the mesh's %v judged by %q, wanted %v by %q", c.what, s.Mesh, s.Judge, c.mesh, c.judge)
|
||||
}
|
||||
if s.gated() != (c.modules != "" || c.new != "") {
|
||||
t.Errorf("%s: gated %v", c.what, s.gated())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A module whose build packages another repository's source is touched by a change to it.
|
||||
func TestAPullRequestTouchesWhatPackagesItsRepository(t *testing.T) {
|
||||
entries := []inventory.Entry{fromRepo("node-tools", "http://forge.internal:20000/novox/mesh-tools.git", "node-tools")}
|
||||
read := map[string][]inventory.ReadRepository{"node-tools": {{Repository: "http://forge.internal:20000/novox/mesh-sdk.git"}}}
|
||||
s := pullScope(link.PullUpdated{Owner: "novox", Repo: "mesh-sdk", Base: "main", Paths: []string{"go/x.go"}}, entries, read)
|
||||
if strings.Join(s.Modules, ",") != "node-tools" || len(s.Manifests) != 0 {
|
||||
t.Fatalf("a change to what node-tools packages touched %v (manifests %v)", s.Modules, s.Manifests)
|
||||
}
|
||||
}
|
||||
|
||||
// Each layer is said; a check that could not run is an error on both, never a pass; a build seat from
|
||||
// before the layers is read as the gate.
|
||||
func TestAChecksLayersAreEachSaidAndAnErrorIsNeverAPass(t *testing.T) {
|
||||
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-catalog", Number: 3, Modules: []string{"gitea"}}
|
||||
c := checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Failed: "the facts snapshot cannot be read"})
|
||||
if c.Verdict != "error" || c.Gate == nil || c.Gate.Verdict != "error" || c.RepoCheck == nil || c.RepoCheck.Verdict != "error" {
|
||||
t.Fatalf("a check that could not run said %+v", c)
|
||||
}
|
||||
if strings.Join(c.Gate.Modules, ",") != "gitea" {
|
||||
t.Errorf("the gate names %v", c.Gate.Modules)
|
||||
}
|
||||
c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "warning", Summary: "wide"}})
|
||||
if c.Gate == nil || c.Gate.Verdict != "warning" || c.RepoCheck != nil {
|
||||
t.Fatalf("an outcome without layers said %+v", c)
|
||||
}
|
||||
c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "pass",
|
||||
Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "fail", Summary: "its merge-check.sh failed"}}})
|
||||
if c.RepoCheck.Verdict != "fail" || c.Gate.Verdict != "pass" {
|
||||
t.Fatalf("the layers said %+v / %+v", c.Gate, c.RepoCheck)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user