From 65508d9df9f45093bc5f3f91e337dbf0801a6f94 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 14 Oct 2026 02:20:37 +0200 Subject: [PATCH] Try a pushed branch on the build seat before its pull request (hq issue 686) Store tests skip without a store and no agent may start one, so each failure cost a 15-30 minute review round. The try verb asks the build seat for the repository's merge-check.sh on the ref merged into the trunk, as a check in the build queue, and reads its outcome from the build's log: no status on the forge, nothing registered, nothing built for a merge. --- cmd/mesh-builder/main.go | 26 ++- cmd/mesh-controller/main.go | 9 + cmd/mesh-controller/seatverbs.go | 28 +++ cmd/mesh-controller/try.go | 309 +++++++++++++++++++++++++++++++ internal/builder/check.go | 23 +++ internal/catalogue/verbs.go | 19 ++ internal/link/build.go | 24 +++ merge-check.sh | 23 ++- module.json | 1 + 9 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 cmd/mesh-controller/try.go diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index 7bf565be..be469d09 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -332,6 +332,12 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri result.Check.Members = append(result.Check.Members, link.CheckedMember{Owner: m.Owner, Repo: m.Repo, Commit: m.Head, Trunk: m.Trunk}) } + // A branch tried before its pull request reads its outcome from this log, whole (novox/hq issue 686). + if request.Check.Try { + if line, err := json.Marshal(triedOf(v)); err == nil { + say(link.StepTried, string(line)) + } + } } } else if err == nil { // The package-registry credential is a build input, so it is resolved before the clone: a @@ -457,7 +463,8 @@ func checkSpecOf(request link.BuildRequest) builder.CheckSpec { 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{}, Modules: c.Modules, New: c.New, Manifests: c.Manifests, Judge: c.Judge, Base: c.Base, - Toolchain: builder.ToolchainOf(request.Held), Toolchains: builder.ToolchainsOf(request.Held)} + Toolchain: builder.ToolchainOf(request.Held), Toolchains: builder.ToolchainsOf(request.Held), + Tests: c.Tests, StoreOnly: c.StoreOnly} for dir, b := range c.Beside { spec.Beside[dir] = builder.Beside{Repository: b.Repository, Ref: b.Ref} } @@ -472,6 +479,23 @@ func checkSpecOf(request link.BuildRequest) builder.CheckSpec { return spec } +// triedOf is a try's outcome as its log line says it (novox/hq issue 686): the repository's own check's verdict, +// or the check's where that did not run — a head that does not merge — and the lines naming what failed. +func triedOf(v builder.CheckVerdict) link.Tried { + t := link.Tried{Verdict: v.Verdict, Summary: v.Summary, Took: v.Took.Round(time.Second).String(), + Head: v.Head, Trunk: v.Trunk} + if v.Repo != nil && (v.Gate == nil || v.Gate.Verdict == "pass") { + t.Verdict, t.Summary = v.Repo.Verdict, v.Repo.Summary + } + if v.Repo != nil && v.Repo.Failed != "" { + t.Failed = strings.Split(v.Repo.Failed, "\n") + } + if t.Verdict == "" { + t.Verdict = "error" + } + return t +} + // checkBuiltOf is a check's builds for its merge as the outcome carries them (novox/hq ADR 0350 §1). func checkBuiltOf(built []builder.CheckBuilt) []link.CheckBuilt { var out []link.CheckBuilt diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index 1e656622..41d7319d 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -106,6 +106,8 @@ func run() error { return killCommand(ctx, args[1:]) case "forget-checks": return forgetChecksCommand(ctx, args[1:]) + case "try": + return tryCommand(ctx, args[1:]) case "pause", "resume": return pauseCommand(ctx, args[0], args[1:]) case "collection": @@ -381,6 +383,13 @@ func (b builds) Built(ctx context.Context, result link.BuildResult) error { // **A merge check builds nothing** (novox/hq to-be 45 §9): its verdict is said, and nothing of it is // recorded or registered. if result.Check != nil || result.Checked != nil { + if isTry(result) { + // A branch tried before its pull request (novox/hq issue 686): its asker reads the outcome from the + // build's log. Never said as `checked`, so the forge's holder sets no status. + fmt.Printf("%s: a try of %s at %.8s, read with `try --id`: said to nobody, nothing taken in\n", result.ID, + result.Repository, result.Ref) + return nil + } checked(ctx, result) return nil } diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index 3cc62d1a..c0c222e3 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -457,6 +457,28 @@ func (a *verbArguments) commandLine() ([]string, error) { return nil, err } return []string{"forget-checks", str("owner"), str("repo"), str("number")}, nil + case "try": + // A branch tried before its pull request (novox/hq issue 686): asked, or read by the id it answered. + if id := str("id"); id != "" { + return []string{"try", "--id", id}, nil + } + if a.given["repository"] == "" && a.given["ref"] == "" { + // Neither shape named: the command says its usage, which names both, as `build` does. + return []string{"try"}, nil + } + if err := need("repository", "ref"); err != nil { + return nil, err + } + argv := []string{"try", str("repository"), str("ref")} + for _, flag := range []string{"base", "tests"} { + if v := str(flag); v != "" { + argv = append(argv, "--"+flag, v) + } + } + if on("store-only") { + argv = append(argv, "--store-only") + } + return argv, nil case "cancel", "kill": if err := need("id"); err != nil { return nil, err @@ -1018,6 +1040,8 @@ var jsonVerbs = map[string]bool{"status": true, "seats": true, "plan": true, "co "delivery": true, // The build queue, each ask with its position among the waiting (novox/hq issue 455). "queue": true, + // A branch tried before its pull request (novox/hq issue 686). + "try": true, // A plan followed (novox/hq issue 229): `plans wait` answers JSON; its other forms say text, and so answer no data. "plans": true} @@ -1308,6 +1332,10 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) { link.AnswersWithin(ctx, bound+planWaitStart) } } + if verb == "try" && len(argv) > 1 && argv[1] == "--id" { + // Reading a try holds its answer for the verdict, up to its bound (novox/hq issue 686, 229). + link.AnswersWithin(ctx, tryWait+planWaitStart) + } if answer, read := readHere(ctx, argv); read { return answer, nil } diff --git a/cmd/mesh-controller/try.go b/cmd/mesh-controller/try.go new file mode 100644 index 00000000..9f586b5e --- /dev/null +++ b/cmd/mesh-controller/try.go @@ -0,0 +1,309 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/nats-io/nats.go" + + "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/link" +) + +// A branch is tried before its pull request (novox/hq issue 686). +// +// A repository's store tests skip without a store, and no agent may start a database on its machine. So a +// builder pushed, a reviewer read, and only the pull request's merge check ran those tests for the first time: +// each failure there cost a round of fifteen to thirty minutes (mesh-controller #283, #310, #312). +// +// `try / ` asks the build seat for the repository's own check of a pushed branch or +// commit, as the merge check runs it (internal/builder/check.go): the ref merged into the trunk's head, the +// trunk's merge-check.sh, each part in its toolchain's container, against a throwaway store and bus. It is a +// check and nothing more: +// +// - **no pull request, so no status**: its outcome is never said as `checked`, which is what the forge's +// holder sets statuses from (isTry, in Built); +// - **nothing registered, nothing built**: it names no module of the graph, so the merge gate does not run and +// no build is made for a merge (novox/hq ADR 0350's scratch builds are a pull request's check's alone); +// - **a check in the build queue**: after deliveries (link.KindOf, novox/hq issue 558), and bounded by the +// check's own bound (checkTimeout); +// - **the code a pull request's check would run anyway**: only a repository of the mesh, whose every pull +// request the build seat already checks, in containers that hold no runtime socket. +// +// A check outlasts a seat verb's one answer, so the ask answers its id at once and `try --id ` reads what +// came of it from the build's log, where the build seat says it whole (link.StepTried): holding its answer up +// to tryWait for a verdict, like `plans wait` (novox/hq issue 229). Nothing is kept beside the log. + +// tryWait is how long reading a try holds its answer for the verdict. A variable so a test need not wait. +var tryWait = 20 * time.Second + +// A ref as the forge is asked for it — a branch or a commit, never an option — and a `go test -run` pattern. +var ( + tryRef = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*$`) + tryTests = regexp.MustCompile(`^[A-Za-z0-9_^$|().*+?/\[\]-]+$`) +) + +// tryAnswer is what `try` answers, asking or reading. +type tryAnswer struct { + ID string `json:"id"` + // State is asked (just now), waiting (nothing said of it yet: in the build queue), running, or the verdict: + // pass, warning (the repository declares no merge-check.sh), fail, or error (it could not run). + State string `json:"state"` + Said string `json:"said"` + Repository string `json:"repository,omitempty"` + Ref string `json:"ref,omitempty"` + Commit string `json:"commit,omitempty"` + Base string `json:"base,omitempty"` + Trunk string `json:"trunk,omitempty"` + On string `json:"on,omitempty"` + Took string `json:"took,omitempty"` + Failed []string `json:"failed,omitempty"` + // Last is the last thing the build seat said of one still running. + Last string `json:"last,omitempty"` +} + +// isTry says a check's outcome is a try's: marked so, or — echoed by a build seat from before the mark — a +// check of no pull request and no group, which is nobody's status either. +func isTry(result link.BuildResult) bool { + c := result.Checked + return c != nil && (c.Try || (c.Number == 0 && c.Group == "")) +} + +// asATry makes a check's ask a try's: the repository's own script alone, for no pull request. +func asATry(request link.BuildRequest, tests string, storeOnly bool) link.BuildRequest { + c := *request.Check + c.Number, c.Try, c.Tests, c.StoreOnly = 0, true, tests, storeOnly + c.Paths, c.Modules, c.New, c.Dependents, c.Manifests, c.Builds = nil, nil, nil, nil, nil, nil + c.Judge, c.Plan, c.Group, c.Members = "", nil, "", nil + request.Check = &c + return request +} + +// forgeCommit asks the forge, through its module's tools on the bus, which commit a branch or commit of a +// repository is, and the repository's default branch. A variable so a test needs no forge. +var forgeCommit = func(ctx context.Context, owner, repo, ref string) (commit, trunk string, err error) { + js, err := aBus() + if err != nil { + return "", "", err + } + defer js.Close() + bus := link.OverNATS{Conn: js.Conn()} + ask := func(path string, into any) error { + raw, _ := json.Marshal(map[string]any{"path": path}) + answer, err := link.Ask(ctx, bus, "gitea", "gitea_api", raw, 30*time.Second) + if err != nil { + return err + } + if answer.Error != "" { + return errors.New(answer.Error) + } + return json.Unmarshal(answer.Result, into) + } + var commits struct { + Result []struct { + SHA string `json:"sha"` + } `json:"result"` + } + if err := ask("/repos/"+owner+"/"+repo+"/commits?limit=1&stat=false&verification=false&files=false&sha="+ + url.QueryEscape(ref), &commits); err != nil { + return "", "", err + } + if len(commits.Result) == 0 || commits.Result[0].SHA == "" { + return "", "", errors.New("the forge named no commit for it") + } + var found struct { + Result struct { + DefaultBranch string `json:"default_branch"` + } `json:"result"` + } + if err := ask("/repos/"+owner+"/"+repo, &found); err != nil { + return "", "", err + } + return commits.Result[0].SHA, found.Result.DefaultBranch, nil +} + +// tryCommand is `try / [--base ] [--tests ] [--store-only]`, and +// `try --id ` to read what came of one. +func tryCommand(ctx context.Context, args []string) error { + set := flag.NewFlagSet("try", flag.ContinueOnError) + base := set.String("base", "", "the branch it is merged into (the repository's default branch when none)") + tests := set.String("tests", "", "only the tests this `go test -run` pattern names") + storeOnly := set.Bool("store-only", false, "only the repository's store tests") + id := set.String("id", "", "a try's id: what came of it") + positionals, err := parseAround(set, args) + if err != nil { + return err + } + usage := errors.New("try / [--base ] [--tests ] " + + "[--store-only], or try --id ") + if *id != "" { + if len(positionals) != 0 { + return usage + } + return tryRead(ctx, *id) + } + if len(positionals) != 2 { + return usage + } + owner, repo, isPath := strings.Cut(positionals[0], "/") + ref := positionals[1] + switch { + case !isPath || !tryRef.MatchString(owner) || !tryRef.MatchString(repo) || strings.Contains(repo, "/"): + return fmt.Errorf("%q is not a repository on the forge, owner/name; nothing was asked", positionals[0]) + case !tryRef.MatchString(ref): + return fmt.Errorf("%q is not a branch or a commit; nothing was asked", ref) + case *base != "" && !tryRef.MatchString(*base): + return fmt.Errorf("%q is not a branch; nothing was asked", *base) + case *tests != "" && !tryTests.MatchString(*tests): + return fmt.Errorf("%q is not a `go test -run` pattern of test names; nothing was asked", *tests) + } + // **An unknown ref is refused here, loudly** — not sent to a machine to fail at its clone minutes later. + commit, trunk, err := forgeCommit(ctx, owner, repo, ref) + if err != nil { + return fmt.Errorf("the forge holds no branch or commit %s in %s/%s — is it pushed? — so nothing was asked: %w", + ref, owner, repo, err) + } + if *base != "" { + trunk = *base + } + if trunk == "" { + return fmt.Errorf("the forge named no default branch for %s/%s, and no --base says what %s is merged into; "+ + "nothing was asked", owner, repo, ref) + } + + open, err := openStores(ctx) + if err != nil { + return err + } + defer open.Close() + entries, read, edges, err := theGraph(ctx, open.inventory) + if err != nil { + return err + } + p := link.PullUpdated{Owner: owner, Repo: repo, Base: trunk, Commit: commit} + // Whose repository it is, as a pull request's check reckons it: only the mesh's is tried, the code the + // build seat runs for every pull request already. + mesh := pullScope(p, entries, read, edges) + if !mesh.Mesh { + return fmt.Errorf("%s/%s is no repository of the mesh — no module is built from it and its owner is not "+ + "the core's — and only the mesh's are tried on the build seat; nothing was asked", owner, repo) + } + request, err := checkRequestFor(ctx, open, p, checkScope{Mesh: true, From: mesh.From}, entries) + if err != nil { + return err + } + request = asATry(request, *tests, *storeOnly) + seat := buildSeatHeld(ctx) + ask, err := askOverOn(seat) + if err != nil { + return err + } + defer ask.Close() + if err := ask.Ask(ctx, request); err != nil { + return err + } + return printTry(tryAnswer{ID: request.ID, State: "asked", Repository: owner + "/" + repo, Ref: ref, Commit: commit, + Base: trunk, On: seat, Said: fmt.Sprintf("asked %s to try %s/%s at %.12s (%s) merged into %s: its own "+ + "merge-check.sh, as a check in the build queue, bounded at %s; no status is set and nothing is registered. "+ + "Read it with try id=%s — a full run of this kind takes minutes", seat, owner, repo, commit, ref, trunk, + checkTimeout, request.ID)}) +} + +// tryRead answers what came of a try, holding the answer up to tryWait for its verdict. +func tryRead(ctx context.Context, id string) error { + until := time.Now().Add(tryWait) + for { + lines, err := buildLines(ctx, id) + if err != nil { + return err + } + a := tryAnswerOf(id, lines) + if (a.State != "waiting" && a.State != "running") || !time.Now().Add(2*time.Second).Before(until) { + return printTry(a) + } + if err := sleepOrDone(ctx, 2*time.Second); err != nil { + return err + } + } +} + +// tryAnswerOf reads a try's answer from what the build seat said of it: its outcome under StepTried; `failed` +// for one that could not run — an error, never a pass; else how far it is. +func tryAnswerOf(id string, lines []link.BuildLine) tryAnswer { + a := tryAnswer{ID: id, State: "waiting", Said: "nothing is said of " + id + " yet: it waits in the build queue " + + "(`queue` says where), or no try has that id; ask again"} + for _, l := range lines { + switch l.Step { + case link.StepTried: + var t link.Tried + if err := json.Unmarshal([]byte(l.Message), &t); err != nil || t.Verdict == "" { + return tryAnswer{ID: id, State: "error", Said: "the build seat's word on it cannot be read: " + l.Message} + } + return tryAnswer{ID: id, State: t.Verdict, Said: t.Summary, Took: t.Took, Commit: t.Head, Trunk: t.Trunk, + Failed: t.Failed} + case "failed": + return tryAnswer{ID: id, State: "error", Said: "it could not run: " + l.Message} + case "checked": + // A check that is no try, or a build seat from before the verb: its verdict is not a try's. + return tryAnswer{ID: id, State: "error", Said: "the build seat checked it and said no try's outcome — " + + "it predates the verb (novox/hq issue 686), or " + id + " is no try: " + l.Message + "; `builds` with " + + "the id says every line"} + } + a.State, a.Last = "running", l.Message + a.Said = "still running; ask again. `builds` with the id says every line so far" + } + return a +} + +func printTry(a tryAnswer) error { + fmt.Fprintln(os.Stderr, a.Said) + for _, l := range a.Failed { + fmt.Fprintln(os.Stderr, " "+l) + } + out, err := json.MarshalIndent(a, "", " ") + if err != nil { + return err + } + fmt.Println(string(out)) + return nil +} + +// buildLines is everything the build seat said of one build, as the bus keeps it, in order. +func buildLines(ctx context.Context, id string) ([]link.BuildLine, error) { + js, err := aBus() + if err != nil { + return nil, fmt.Errorf("cannot reach the bus to read a build's log: %w", err) + } + defer js.Close() + subject := link.BuildLogOf("*", id) + sub, err := js.Context().PullSubscribe(subject, "", + nats.BindStream(broker.EventsStream), nats.DeliverAll(), nats.AckNone()) + if err != nil { + return nil, fmt.Errorf("cannot read %s from the bus: %w", subject, err) + } + defer func() { _ = sub.Unsubscribe() }() + var lines []link.BuildLine + for { + batch, err := sub.Fetch(200, nats.MaxWait(2*time.Second)) + if err != nil && !errors.Is(err, nats.ErrTimeout) && !errors.Is(err, context.DeadlineExceeded) { + return nil, fmt.Errorf("reading a build's log: %w", err) + } + for _, msg := range batch { + var line link.BuildLine + if json.Unmarshal(msg.Data, &line) == nil { + lines = append(lines, line) + } + } + if len(batch) < 200 { + return lines, nil + } + } +} diff --git a/internal/builder/check.go b/internal/builder/check.go index bf0ce5bc..a2107499 100644 --- a/internal/builder/check.go +++ b/internal/builder/check.go @@ -103,6 +103,23 @@ type CheckSpec struct { // for the walk to take. None for a group's composed check, whose members' own checks build. Builds []CheckBuildSpec BuildWith *CheckBuilding + // Tests and StoreOnly narrow what the repository's own script runs, for a branch tried before its pull + // request (novox/hq issue 686): handed to the script as EnvTests and EnvStoreOnly, which it may read. + Tests string + StoreOnly bool +} + +// selection is what a check hands the repository's script to narrow what it runs (novox/hq issue 686): nothing +// for a pull request's check, which always runs whole. +func selection(spec CheckSpec) []string { + var env []string + if spec.Tests != "" { + env = append(env, EnvTests+"="+spec.Tests) + } + if spec.StoreOnly { + env = append(env, EnvStoreOnly+"=1") + } + return env } // GroupHead is one other head of a delivery group's composed check. @@ -193,6 +210,11 @@ const ( // EnvGroup is a delivery group's other heads, as JSON, for the gate (novox/hq ADR 0239); empty for a // pull request checked alone. EnvGroup = "MESH_CHECK_GROUP" + // EnvTests is a `go test -run` pattern and EnvStoreOnly is 1 when a branch tried before its pull request + // asks for only those tests, or only the store tests (novox/hq issue 686). Never set for a pull request's + // check. A script that reads neither runs whole. + EnvTests = "MESH_CHECK_TESTS" + EnvStoreOnly = "MESH_CHECK_STORE_ONLY" ) // CheckTimeout bounds one check; a check that runs past it is an error, not a pass. @@ -515,6 +537,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","), EnvGroup + "=" + groupFile, "GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")} + env = append(env, selection(spec)...) v := CheckVerdict{} modules := append(append([]string{}, spec.Modules...), prefixed("new:", spec.New)...) diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index 673f62f4..669b7758 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -362,6 +362,25 @@ var ControllerVerbs = []Verb{ "register": "\"true\" to register what it builds", "older": "\"true\", with register: even though a newer build of the module is registered", }, []string{"id"}, "register", "older")}, + // novox/hq issue 686: a builder ran a branch's store tests for the first time in its pull request's merge check. + {Name: "try", Description: "Try a pushed branch or commit before its pull request (novox/hq issue 686): the build " + + "seat merges it into the repository's trunk and runs the repository's own merge-check.sh on the merge, in the " + + "toolchain containers and against the throwaway store and bus a pull request's merge check gets. Answers at " + + "once with an id; asked again with that id it waits up to 20 s and answers pass, fail or error, how long it " + + "took and the lines naming what failed — or that it is still waiting or running, to ask again. Sets no " + + "status on the forge, registers and builds nothing, runs no merge gate, counts as a check in the build queue " + + "(after deliveries) and is bounded as one. Only for a repository of the mesh; the ref must be on the forge.", + Input: schema(map[string]string{ + "repository": "the repository on the forge, owner/name; with ref", + "ref": "the branch or commit to try, as pushed to the forge", + "base": "the branch it is merged into (the repository's default branch when none is named)", + "tests": "only the tests this `go test -run` pattern names, where the repository's merge-check.sh reads " + + "MESH_CHECK_TESTS; a script that does not runs whole", + "store-only": "\"true\": only the repository's store tests, where its merge-check.sh reads " + + "MESH_CHECK_STORE_ONLY", + "id": "instead of a repository: the id a try answered, to read what came of it", + }, nil, "store-only"), + Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, // novox/hq ADR 0350 §5: asked by the delivery's owner when the forge says a pull request closed unmerged. {Name: "forget-checks", Description: "Let go of every build a pull request's merge checks made for its merge in " + "the artifact store's scratch namespace (novox/hq ADR 0350 §5): asked when it closed unmerged; a merged one's go " + diff --git a/internal/link/build.go b/internal/link/build.go index 6eb16588..73384e9f 100644 --- a/internal/link/build.go +++ b/internal/link/build.go @@ -164,6 +164,30 @@ type CheckRequest struct { // each, from the merge it judged, into the scratch namespace once it has judged and not failed, for the walk to // take. A build seat from before it builds nothing, and the walk builds as before. Builds []CheckBuild `json:"builds,omitempty"` + // Try marks a branch tried before its pull request (novox/hq issue 686): the repository's own check for a + // pushed branch or commit, asked by a builder through the controller's `try` verb. Its outcome is read from + // the build's log (StepTried) and said to nobody: no status on the forge, nothing recorded or registered, + // nothing built for a merge. Tests narrows what the repository's script runs to the tests a pattern names, + // and StoreOnly to its store tests; a script that reads neither runs whole. + Try bool `json:"try,omitempty"` + Tests string `json:"tests,omitempty"` + StoreOnly bool `json:"store-only,omitempty"` +} + +// StepTried is the step a build seat says a try's outcome under, in the build's log: one line, a Tried as JSON +// (novox/hq issue 686). +const StepTried = "tried" + +// Tried is what came of a branch tried before its pull request (novox/hq issue 686): the verdict of the +// repository's own check for the ref merged into the trunk, how long it took, and the lines that name what +// failed, picked from the whole of its output (issue 460). +type Tried struct { + Verdict string `json:"verdict"` + Summary string `json:"summary"` + Took string `json:"took"` + Head string `json:"head,omitempty"` + Trunk string `json:"trunk,omitempty"` + Failed []string `json:"failed,omitempty"` } // CheckBuild is one module a check builds for its merge: from the merge it made when Merged — a module built from diff --git a/merge-check.sh b/merge-check.sh index e1e85687..36de3711 100755 --- a/merge-check.sh +++ b/merge-check.sh @@ -54,11 +54,26 @@ if [ -n "$unformatted" ]; then fi CGO_ENABLED=0 go vet ./... -if command -v gcc >/dev/null 2>&1; then - CGO_ENABLED=1 go test -race -count=1 -timeout 30m ./... +suite() { + if command -v gcc >/dev/null 2>&1; then + CGO_ENABLED=1 go test -race -count=1 -timeout 30m "$@" + else + echo "NOT RACE-CHECKED: the toolchain holds no C compiler; the suite runs without the race detector" + CGO_ENABLED=0 go test -count=1 -timeout 30m "$@" + fi +} +# **A branch tried before its pull request may ask for a part** (novox/hq issue 686): the controller's `try` verb +# hands MESH_CHECK_TESTS, a `go test -run` pattern, or MESH_CHECK_STORE_ONLY=1. A pull request's merge check is +# handed neither and always runs whole; a part that passes is said as a part, never as the check. +if [ -n "${MESH_CHECK_TESTS:-}" ]; then + echo "TRIED IN PART: only the tests matching $MESH_CHECK_TESTS, in every package" + suite -run "$MESH_CHECK_TESTS" ./... + exit 0 +fi +if [ "${MESH_CHECK_STORE_ONLY:-}" = 1 ]; then + echo "TRIED IN PART: only the store tests listed in testdata/store-tests" else - echo "NOT RACE-CHECKED: the toolchain holds no C compiler; the suite runs without the race detector" - CGO_ENABLED=0 go test -count=1 -timeout 30m ./... + suite ./... fi # **The store tests listed ran** (novox/hq issue 459): a test that needs the store skips without one, and a skip diff --git a/module.json b/module.json index 1b84a8c4..538f5cca 100644 --- a/module.json +++ b/module.json @@ -77,6 +77,7 @@ "replay", "kill", "forget-checks", + "try", "pause", "resume", "hand-act",