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).
136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
|
)
|
|
|
|
// Config values held for the keys an open pull request introduces (novox/hq ADR 0502 clause 4, issue 1826): the gate
|
|
// lays each approved one over its module's values on its node in the composition with the change, and in that one
|
|
// only. In the mesh as it is the key is not declared, so the value would be refused there.
|
|
|
|
// heldValue is one approved held value as the build agent writes it (--held): the value of Key for Module on Node,
|
|
// a machine's real name, which the snapshot holds only as a pseudonym.
|
|
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"`
|
|
}
|
|
|
|
// readHeld reads the file --held names; none named, none held.
|
|
func readHeld(path string) ([]heldValue, error) {
|
|
if path == "" {
|
|
return nil, nil
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var held []heldValue
|
|
if err := json.Unmarshal(raw, &held); err != nil {
|
|
return nil, fmt.Errorf("the held values are not readable: %w", err)
|
|
}
|
|
for _, h := range held {
|
|
if h.Node == "" || h.Module == "" || h.Key == "" || len(h.Value) == 0 {
|
|
return nil, fmt.Errorf("a held value of %s names no node, module, key or value", h.Pull)
|
|
}
|
|
}
|
|
return held, nil
|
|
}
|
|
|
|
// layHeld is the snapshot with each held value laid as the value of its key in its node's layer of its module, and what
|
|
// it said of them. The snapshot given is not changed. **A held value for a machine the snapshot does not hold, or a
|
|
// module the change does not define, is said and not laid**, never dropped silently; one for a module not assigned to
|
|
// its machine is laid and said. The notes name machines as the snapshot does (roles and pseudonym), not by their names.
|
|
func layHeld(f snapshot.Facts, held []heldValue, shelf map[string]catalogue.Manifest) (snapshot.Facts, []string) {
|
|
if len(held) == 0 {
|
|
return f, nil
|
|
}
|
|
out := f
|
|
out.Machines = make([]snapshot.Machine, len(f.Machines))
|
|
copy(out.Machines, f.Machines)
|
|
var notes, laid []string
|
|
pulls := map[string]bool{}
|
|
for _, h := range held {
|
|
at := -1
|
|
pseudonym := snapshot.Pseudonym("machine", h.Node)
|
|
for i, m := range out.Machines {
|
|
if m.Name == pseudonym {
|
|
at = i
|
|
}
|
|
}
|
|
if at < 0 {
|
|
notes = append(notes, fmt.Sprintf("the value held for %s of %s (%s=%s) is not composed: the snapshot holds no machine %s",
|
|
h.Module, h.Pull, h.Key, h.Value, pseudonym))
|
|
continue
|
|
}
|
|
if _, defined := shelf[h.Module]; !defined {
|
|
notes = append(notes, fmt.Sprintf("the value held for %s of %s (%s=%s) is not composed: the change defines no module %s",
|
|
h.Module, h.Pull, h.Key, h.Value, h.Module))
|
|
continue
|
|
}
|
|
var value any
|
|
if err := json.Unmarshal(h.Value, &value); err != nil {
|
|
notes = append(notes, fmt.Sprintf("the value held for %s of %s (%s) is not composed: it is not a value: %v",
|
|
h.Module, h.Pull, h.Key, err))
|
|
continue
|
|
}
|
|
m := &out.Machines[at]
|
|
layers := make([]snapshot.Settings, len(m.Settings))
|
|
copy(layers, m.Settings)
|
|
found := false
|
|
for i := range layers {
|
|
if layers[i].Module == h.Module {
|
|
values := make(map[string]any, len(layers[i].Values)+1)
|
|
for k, v := range layers[i].Values {
|
|
values[k] = v
|
|
}
|
|
values[h.Key] = value
|
|
layers[i].Values = values
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
layers = append(layers, snapshot.Settings{Module: h.Module, Values: map[string]any{h.Key: value}})
|
|
sort.Slice(layers, func(a, b int) bool { return layers[a].Module < layers[b].Module })
|
|
}
|
|
m.Settings = layers
|
|
assigned := false
|
|
for _, a := range m.Assigned {
|
|
assigned = assigned || a == h.Module
|
|
}
|
|
by := ""
|
|
if h.Approved != "" {
|
|
by = ", approved by " + h.Approved
|
|
}
|
|
line := fmt.Sprintf("%s on %s %s=%s%s", h.Module, m.Described(), h.Key, h.Value, by)
|
|
if !assigned {
|
|
line += " (not assigned there in the snapshot)"
|
|
}
|
|
laid = append(laid, line)
|
|
pulls[h.Pull] = true
|
|
}
|
|
if len(laid) > 0 {
|
|
var names []string
|
|
for p := range pulls {
|
|
names = append(names, p)
|
|
}
|
|
sort.Strings(names)
|
|
notes = append([]string{fmt.Sprintf("composed with %d value(s) held for %s: %s", len(laid), strings.Join(names, ", "),
|
|
strings.Join(laid, "; "))}, notes...)
|
|
}
|
|
return out, notes
|
|
}
|