Publish only a commit on its module's trunk; post a pull request's change plan (hq ADR 0238)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/delivery delivered
mesh/delivery-group group feat/the-graph-decides-what-is-checked delivered: every member is delivered

One commit, one plan: a commit off the trunk — a pull request's head, a branch built by
hand, a rebuild or replay of one — is for checking. The build seat reads from its clone
which branches hold the commit, and the controller records and never registers a build
whose commit is not on the branch the module follows (the repository's default for a
new one), so nothing off the trunk can be sent.

A pull request's check now carries its change plan, computed by the planner: what a
merge would build in which order, what each machine would receive, and what is not an
ordinary send — the bus step, a module waiting for a person, a provider's consumers.
This commit is contained in:
jochen
2026-10-06 22:49:55 +02:00
parent 58ebe590a5
commit 3d05d74400
11 changed files with 481 additions and 3 deletions
+63 -1
View File
@@ -68,6 +68,15 @@ type Result struct {
// change to this module (novox/hq 04-ISSUES/131).
Read []catalogue.ArtifactContext
// Trunk is the repository's default branch as the forge holds it at the build, and OnTrunk whether
// the commit built is reachable from it (novox/hq ADR 0238): a commit off the trunk is checked,
// never published. Trunk empty is "could not be said".
Trunk string
OnTrunk bool
// Branches are every branch of the forge the commit is on: a module may follow a branch other than
// the default, and that branch is its trunk.
Branches []string
// Source is the build's source fingerprint (source.go): what it was made from — the module's tree,
// the contexts' trees, the bases and toolchains by digest — hashed. Empty where the source does not
// pin the build. Two builds with one fingerprint are one build, whatever digests they made
@@ -144,6 +153,16 @@ func Build(ctx context.Context, run Runner, publish Publisher,
}
commit = strings.TrimSpace(commit)
say("commit", "%s", short(commit))
// **Whether the commit is on the trunk** (novox/hq ADR 0238): only a commit on the repository's own
// default branch is published. Read from the clone just made — the forge's own word on which branch
// is its default and what it holds now — and said with the outcome, so the controller refuses to
// register a build of a commit off it.
trunk, onTrunk := trunkOf(ctx, run, tree, commit)
branches := branchesHolding(ctx, run, tree, commit)
if trunk != "" {
say("trunk", "%s is %son %s; on %s", short(commit), map[bool]string{true: "", false: "NOT "}[onTrunk], trunk,
orNoBranch(branches))
}
// A module is a repository and a path within it (novox/hq ADR 0069). The ordinary case is an
// empty path, meaning the repository's root; a repository holding several modules names each
@@ -250,7 +269,50 @@ func Build(ctx context.Context, run Runner, publish Publisher,
say("source", "no source fingerprint: %s", orNoTree(src.unpinned))
}
return Result{Manifest: resolved, Commit: commit, Built: built,
Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint}, nil
Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint,
Trunk: trunk, OnTrunk: onTrunk, Branches: branches}, nil
}
// branchesHolding is every branch of a fresh clone's origin the commit is on, without `origin/`.
func branchesHolding(ctx context.Context, run Runner, clone, commit string) []string {
out, err := run(ctx, clone, "git", "branch", "--remotes", "--format=%(refname:short)", "--contains", commit)
if err != nil {
// empty-on-error: no branch said is the commit on none, which refuses its registration — never a pass
return nil
}
var branches []string
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if b, ok := strings.CutPrefix(line, "origin/"); ok && b != "HEAD" && b != "" {
branches = append(branches, b)
}
}
sort.Strings(branches)
return branches
}
func orNoBranch(branches []string) string {
if len(branches) == 0 {
return "no branch"
}
return strings.Join(branches, ", ")
}
// trunkOf is a fresh clone's trunk — the branch the forge names its default, as `origin/HEAD` says — and
// whether a commit is reachable from it. Empty when the clone does not say, which is "not known", never
// "on it".
func trunkOf(ctx context.Context, run Runner, clone, commit string) (string, bool) {
head, err := run(ctx, clone, "git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD")
if err != nil {
return "", false
}
remote := strings.TrimSpace(head)
trunk := strings.TrimPrefix(remote, "origin/")
if trunk == "" || trunk == remote {
return "", false
}
_, err = run(ctx, clone, "git", "merge-base", "--is-ancestor", commit, remote)
return trunk, err == nil
}
// orNoTree is why a build has no source fingerprint, for its log.