Carry a config value held for a pull request's new key into its composed check (issue 1826)

The controller tells mesh-config what a pull request declares, asks the approved held values with each check, and the build agent and merge gate lay them over the change's composition only: a key the trunk does not declare yet can then be approved before the merge (ADR 0502).
This commit is contained in:
2026-10-22 11:01:12 +02:00
parent 3d4ad6fefb
commit 03927a6f6c
14 changed files with 815 additions and 4 deletions
+46 -2
View File
@@ -123,8 +123,41 @@ type CheckSpec struct {
// controller could not learn them, and the verdict says so.
Siblings []Sibling
SiblingsUnknown string
// Held are the approved held values of the check's pull requests (novox/hq ADR 0502 clause 4): written to a file and
// handed to the gate, which lays them over the change's composition only. None: no flag, no file.
Held []HeldValue
}
// HeldValue is one approved config value held for a key an open pull request introduces: the value of Key for Module on
// Node (a machine's real name; the gate finds it in the snapshot by its pseudonym). The gate reads this document.
type HeldValue struct {
Pull string `json:"pull"`
Head string `json:"head,omitempty"`
Node string `json:"node"`
Module string `json:"module"`
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Approved string `json:"approved,omitempty"`
By string `json:"by,omitempty"`
At string `json:"at,omitempty"`
}
// writeHeld writes the held values to held.json under root and answers its path; empty, and nothing written, for none.
func writeHeld(root string, held []HeldValue) (string, error) {
if len(held) == 0 {
return "", nil
}
raw, err := json.Marshal(held)
if err != nil {
return "", err
}
file := filepath.Join(root, "held.json")
return file, os.WriteFile(file, raw, 0o644)
}
// heldFlag is how the gate is given the held values: only when there are some, so a judge from before --held still runs.
const heldFlag = `${MESH_CHECK_HELD:+--held "$MESH_CHECK_HELD"}`
// Sibling is one other open pull request of a delivery group (novox/hq ADR 0382): the repository it is in as
// owner/repo, its number, its whole head, the branch it merges into, and Dir, the directory the check finds that
// repository under beside it (an entry of Beside).
@@ -272,6 +305,8 @@ 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"
// EnvHeld is the file of the approved held values of the check's pull requests (novox/hq ADR 0502); empty for none.
EnvHeld = "MESH_CHECK_HELD"
// 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.
@@ -572,6 +607,15 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
}
}
// The approved held values of the check's pull requests, for the gate (novox/hq ADR 0502 clause 4).
heldFile, err := writeHeld(root, spec.Held)
if err != nil {
return CheckVerdict{}, err
}
if heldFile != "" {
say("check", "with %d config value(s) held for its pull request(s)", len(spec.Held))
}
// Every clone is made: the credential is gone before anything of the check runs (novox/hq issue 462).
forget()
@@ -733,7 +777,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
EnvBeside + "=" + root,
EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","),
EnvGroup + "=" + groupFile,
EnvGroup + "=" + groupFile, EnvHeld + "=" + heldFile,
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")}
env = append(env, changedEnv...)
env = append(env, selection(spec)...)
@@ -1100,7 +1144,7 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
if err := running(inToolchain(tree, append(env, EnvVerdict+"="+verdictFile), "sh", "-c",
`"$MESH_GATE" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" `+
`--repository "$MESH_CHECK_REPOSITORY" --tree . `+gateChangedFlag(env)+` `+
`${MESH_CHECK_GROUP:+--group "$MESH_CHECK_GROUP"} --json > "$MESH_CHECK_VERDICT"`)); err != nil {
`${MESH_CHECK_GROUP:+--group "$MESH_CHECK_GROUP"} `+heldFlag+` --json > "$MESH_CHECK_VERDICT"`)); err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the merge gate"
}
+33
View File
@@ -0,0 +1,33 @@
package builder
import (
"os"
"path/filepath"
"strings"
"testing"
)
// 1826/AC2: the held values are written whole to held.json, and the gate is given the flag only when there are some.
func TestHeldValuesAreWrittenForTheGateAndNoneWriteNothing(t *testing.T) {
root := t.TempDir()
if file, err := writeHeld(root, nil); err != nil || file != "" {
t.Fatalf("none wrote %q %v", file, err)
}
if _, err := os.Stat(filepath.Join(root, "held.json")); err == nil {
t.Fatal("none wrote a file")
}
file, err := writeHeld(root, []HeldValue{{Pull: "novox/mesh-catalog#434", Node: "ace", Module: "n8n", Key: "media-gid",
Value: []byte("2000"), Approved: "the operator"}})
if err != nil || file != filepath.Join(root, "held.json") {
t.Fatalf("wrote %q %v", file, err)
}
raw, _ := os.ReadFile(file)
for _, want := range []string{`"node":"ace"`, `"module":"n8n"`, `"key":"media-gid"`, `"value":2000`} {
if !strings.Contains(string(raw), want) {
t.Fatalf("%s lacks %s", raw, want)
}
}
if !strings.Contains(heldFlag, `--held "$MESH_CHECK_HELD"`) || !strings.HasPrefix(heldFlag, "${MESH_CHECK_HELD:+") {
t.Fatalf("the flag is %s", heldFlag)
}
}