diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index 0eb8215..5928a12 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -171,7 +171,7 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri result := link.BuildResult{ ID: request.ID, Repository: request.Repository, Path: request.Path, - Ref: request.Ref, On: on, Source: request.Source, + Ref: request.Ref, On: on, Source: request.Source, DryRun: request.DryRun, } what := "building " + request.Repository if request.Path != "" { diff --git a/cmd/mesh-controller/build.go b/cmd/mesh-controller/build.go index 3869e42..6172e39 100644 --- a/cmd/mesh-controller/build.go +++ b/cmd/mesh-controller/build.go @@ -592,6 +592,7 @@ func buildAndShow(ctx context.Context, source buildSource, path, ref string, wai ID: link.NewBuildID(time.Now()), Repository: repository, Path: path, Ref: ref, Held: heldBy(ctx), Seats: seatBases(ctx), + DryRun: true, }, wait) if err != nil { return err diff --git a/cmd/mesh-controller/dry_run_test.go b/cmd/mesh-controller/dry_run_test.go new file mode 100644 index 0000000..0f08734 --- /dev/null +++ b/cmd/mesh-controller/dry_run_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/novox/mesh-controller/internal/link" +) + +// A dry run's outcome is looked at, never taken in (novox/hq issue 240). The daemon here holds no +// store at all, so anything that tried to record or register would fail rather than pass quietly. +func TestADryRunsOutcomeIsTakenInByNothing(t *testing.T) { + err := builds{}.Built(t.Context(), link.BuildResult{ + ID: "build-1", Repository: "ssh://forge/app.git", Ref: "unreviewed", Module: "app", DryRun: true, + Manifest: json.RawMessage(`{"module":"app","version":"1"}`), + }) + if err != nil { + t.Fatalf("a dry run's outcome was not simply set aside: %v", err) + } +} + +// The mark survives the wire both ways: asked as a dry run, answered as one. +func TestTheDryRunMarkTravelsWithTheBuild(t *testing.T) { + raw, _ := json.Marshal(link.BuildRequest{ID: "build-1", Repository: "r", DryRun: true}) + var asked link.BuildRequest + if err := json.Unmarshal(raw, &asked); err != nil || !asked.DryRun { + t.Fatalf("the request lost its dry-run mark: %s", raw) + } + raw, _ = json.Marshal(link.BuildResult{ID: "build-1", DryRun: true}) + var answered link.BuildResult + if err := json.Unmarshal(raw, &answered); err != nil || !answered.DryRun { + t.Fatalf("the outcome lost its dry-run mark: %s", raw) + } + raw, _ = json.Marshal(link.BuildResult{ID: "build-2"}) + if string(raw) != `{"id":"build-2","repository":"","on":""}` { + t.Fatalf("an ordinary outcome carries a dry-run mark: %s", raw) + } +} diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index baa66f4..0f2561f 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -253,6 +253,13 @@ func parseAround(set *flag.FlagSet, args []string) ([]string, error) { // registered, the same as the waiting command does. Said either way, so the daemon's log tells what // became of a build nobody was watching. func (b builds) Built(ctx context.Context, result link.BuildResult) error { + // **A dry run is looked at, never taken in** (novox/hq issue 240). On 2026-10-04 a dry run of an + // unmerged branch was heard here like any build, registered, and its definition reached a machine + // before anyone had reviewed it. + if result.DryRun { + fmt.Printf("%s: a dry run of %s on %s, not taken in\n", result.ID, result.Repository, result.Ref) + return nil + } manifest, _, err := takeIn(ctx, b.inv, result) // When it was asked, so a plan takes as its outcome only a build asked for it or after it // (novox/hq 04-ISSUES/219). Zero when the id does not say. diff --git a/internal/link/build.go b/internal/link/build.go index c7b0500..c866e75 100644 --- a/internal/link/build.go +++ b/internal/link/build.go @@ -83,6 +83,10 @@ type BuildRequest struct { // asking and echoed in the outcome, so whoever hears the outcome can register the module with // its true source, whether or not they were the one who asked (novox/hq issue 176). Source *SourceOnSeat `json:"source,omitempty"` + // DryRun says the asker wants the outcome to look at and nothing else (novox/hq issue 240): the + // builder echoes it, and whoever hears the outcome takes nothing in — no record, no registration, + // no plan, nothing a push could send. + DryRun bool `json:"dry-run,omitempty"` } // SourceOnSeat names a repository by the seat whose holder serves it and its path there. @@ -144,6 +148,9 @@ type BuildResult struct { // Source is the request's, echoed: the seat form of the repository, for whoever registers. Source *SourceOnSeat `json:"source,omitempty"` + + // DryRun is the request's, echoed: an outcome nobody may take in (novox/hq issue 240). + DryRun bool `json:"dry-run,omitempty"` } // ReadRepository is a repository a build read source from besides the module's own, at the branch,