Draw a plan's order per module from the graph and the manifests' change, and read no after: line (issue 977, part B, ADR 0419)
This commit is contained in:
+50
-177
@@ -579,8 +579,6 @@ type orderMember struct {
|
||||
ModuleDirs []string `json:"module_dirs,omitempty"`
|
||||
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
|
||||
CloneURL string `json:"clone_url,omitempty"`
|
||||
// After are the repositories its pull request says it goes after (`after: <repository>`).
|
||||
After []string `json:"after,omitempty"`
|
||||
// BaseSHA is the base branch's head the forge last announced the head with (novox/hq ADR 0323): the trunk commit
|
||||
// a verdict no build judges names when the head is checked again through mesh-delivery.
|
||||
BaseSHA string `json:"base_sha,omitempty"`
|
||||
@@ -611,14 +609,6 @@ type orderPair struct {
|
||||
Over []string `json:"over,omitempty"`
|
||||
}
|
||||
|
||||
// orderContradiction is two members that rules of one rank order both ways: no precedence resolves it, so
|
||||
// the group is refused, the rules named, and Said says how a person declares the order.
|
||||
type orderContradiction struct {
|
||||
Members []string `json:"members"`
|
||||
Rules []orderPair `json:"rules"`
|
||||
Said string `json:"said"`
|
||||
}
|
||||
|
||||
// orderReach is what a member moves, as the planner says.
|
||||
type orderReach struct {
|
||||
Moved []string `json:"moved,omitempty"`
|
||||
@@ -627,114 +617,76 @@ type orderReach struct {
|
||||
Manifests []string `json:"manifests,omitempty"`
|
||||
}
|
||||
|
||||
// groupOrder is a group's order: the members in it, every pair and why, and the cycle when there is one —
|
||||
// with each contradiction no precedence resolves.
|
||||
// groupOrder is a group's order: the members in it, every pair and why, and the cycle when there is one. The
|
||||
// order is read from the module graph and the manifests' change alone (novox/hq ADR 0419): a cycle is the only
|
||||
// refusal, and CycleSaid names every module and edge of it. Edges are the module edges the pairs come from.
|
||||
type groupOrder struct {
|
||||
Order []string `json:"order"`
|
||||
Pairs []orderPair `json:"pairs,omitempty"`
|
||||
Cycle []string `json:"cycle,omitempty"`
|
||||
Contradictions []orderContradiction `json:"contradictions,omitempty"`
|
||||
Reach map[string]orderReach `json:"reach,omitempty"`
|
||||
Order []string `json:"order"`
|
||||
Pairs []orderPair `json:"pairs,omitempty"`
|
||||
Cycle []string `json:"cycle,omitempty"`
|
||||
CycleSaid string `json:"cycle_said,omitempty"`
|
||||
Edges []inventory.Edge `json:"edges,omitempty"`
|
||||
Reach map[string]orderReach `json:"reach,omitempty"`
|
||||
}
|
||||
|
||||
// The reasons a pair is ordered, in the words the delivery plan shows.
|
||||
const (
|
||||
orderDeclared = "declared"
|
||||
orderBuiltBy = "built by"
|
||||
orderVersionSkew = "version skew"
|
||||
orderEngineFirst = "engine before controller"
|
||||
)
|
||||
// orderBuiltBy is the reason a member goes before another that moves a module built by, standing on, packaging or
|
||||
// declaring one of its modules: the module graph's own edge.
|
||||
const orderBuiltBy = "built by"
|
||||
|
||||
// orderRank is a rule's precedence when two rules order one pair both ways (novox/hq ADR 0249), the lower
|
||||
// winning: what a person declared; then what the graph and the change say (built by, version skew); then
|
||||
// the rollout default, which infers a direction from the modules' names alone.
|
||||
func orderRank(why string) int {
|
||||
switch why {
|
||||
case orderDeclared:
|
||||
return 0
|
||||
case orderBuiltBy, orderVersionSkew:
|
||||
return 1
|
||||
case orderEngineFirst:
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
// orderOf is a group's order (novox/hq ADR 0239 decision 4, ADR 0249), pure. A member goes before another
|
||||
// when:
|
||||
//
|
||||
// - its pull request is named in the other's `after:` lines (declared);
|
||||
// - it moves a module the other's moved modules are built by, stand on, package or declare (built by);
|
||||
// - it moves the controller and the other changes any module's manifest (version skew: the newer
|
||||
// controller parses what the newer manifest says) — the node-engine's excepted, which goes first;
|
||||
// - it moves the node-engine and the other moves the controller (the witness reads nothing the controller
|
||||
// does not yet grant, and a controller sends nothing an older engine would refuse — ADR 0236's rollout
|
||||
// order).
|
||||
//
|
||||
// When rules order one pair both ways, the one of higher precedence wins (orderRank) and the pair says
|
||||
// what it won over. Rules of one rank both ways are a contradiction: both are kept as pairs, the members
|
||||
// are the cycle, and the contradiction says how to declare the order. Otherwise, by repository then id, so
|
||||
// every reading gives one order. A cycle through three members or more is named by its members, and none
|
||||
// of them is ordered.
|
||||
func orderOf(members []orderMember, reach map[string]orderReach, edges []inventory.Edge) groupOrder {
|
||||
// orderOf is a group's order (novox/hq ADR 0419), pure: a member goes before another when a module it moves goes
|
||||
// before a module the other moves by an edge of the module graph (built by), or by a rule of delivery_edges.go
|
||||
// (verb before caller, version skew, engine before controller), which changes names the manifests' added verbs.
|
||||
// No `after:` line is read. Ties break by repository then id, so every reading gives one order. A cycle through
|
||||
// two members or more is named by its members, and none of them is ordered.
|
||||
func orderOf(members []orderMember, reach map[string]orderReach, edges []inventory.Edge, changes map[string]moduleChange) groupOrder {
|
||||
out := groupOrder{Reach: reach}
|
||||
byID := map[string]orderMember{}
|
||||
movedBy := map[string][]string{}
|
||||
var all []string
|
||||
for _, m := range members {
|
||||
byID[m.ID] = m
|
||||
for _, n := range reach[m.ID].Moved {
|
||||
movedBy[n] = append(movedBy[n], m.ID)
|
||||
all = appendNew(all, n)
|
||||
}
|
||||
}
|
||||
var claims []orderPair
|
||||
add := func(before, after, why string) {
|
||||
if before == after {
|
||||
return
|
||||
}
|
||||
for _, p := range claims {
|
||||
for _, p := range out.Pairs {
|
||||
if p.Before == before && p.After == after && p.Why == why {
|
||||
return
|
||||
}
|
||||
}
|
||||
claims = append(claims, orderPair{Before: before, After: after, Why: why})
|
||||
out.Pairs = append(out.Pairs, orderPair{Before: before, After: after, Why: why})
|
||||
}
|
||||
named := func(said, repository string) bool {
|
||||
said = strings.TrimSuffix(strings.TrimSpace(said), ".git")
|
||||
if strings.EqualFold(said, repository) {
|
||||
return true
|
||||
for _, e := range edges {
|
||||
if !orders(e) {
|
||||
continue
|
||||
}
|
||||
_, repo, _ := strings.Cut(repository, "/")
|
||||
return strings.EqualFold(said, repo)
|
||||
for _, before := range movedBy[e.To] {
|
||||
for _, after := range movedBy[e.From] {
|
||||
add(before, after, orderBuiltBy)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range members {
|
||||
for _, b := range members {
|
||||
if a.ID == b.ID {
|
||||
continue
|
||||
}
|
||||
ra, rb := reach[a.ID], reach[b.ID]
|
||||
for _, said := range b.After {
|
||||
if named(said, a.Repository) {
|
||||
add(a.ID, b.ID, orderDeclared)
|
||||
}
|
||||
}
|
||||
for _, e := range edges {
|
||||
if slices.Contains(ra.Moved, e.To) && slices.Contains(rb.Moved, e.From) && e.From != e.To {
|
||||
add(a.ID, b.ID, orderBuiltBy)
|
||||
break
|
||||
}
|
||||
}
|
||||
if slices.Contains(ra.Moved, "mesh-controller") && len(rb.Manifests) > 0 &&
|
||||
!slices.Contains(rb.Moved, "mesh-controller") && !slices.Contains(rb.Moved, "mesh-host") {
|
||||
add(a.ID, b.ID, orderVersionSkew)
|
||||
}
|
||||
if slices.Contains(ra.Moved, "mesh-host") && slices.Contains(rb.Moved, "mesh-controller") &&
|
||||
!slices.Contains(ra.Moved, "mesh-controller") {
|
||||
add(a.ID, b.ID, orderEngineFirst)
|
||||
order := orderEdges(all, changes, edges)
|
||||
out.Edges = order.Edges
|
||||
out.CycleSaid = order.CycleSaid()
|
||||
for _, e := range order.Edges {
|
||||
for _, before := range movedBy[e.To] {
|
||||
for _, after := range movedBy[e.From] {
|
||||
add(before, after, e.Why)
|
||||
}
|
||||
}
|
||||
}
|
||||
out.Pairs, out.Contradictions = resolveClaims(claims, byID)
|
||||
sort.Slice(out.Pairs, func(i, j int) bool {
|
||||
if out.Pairs[i].Before != out.Pairs[j].Before {
|
||||
return out.Pairs[i].Before < out.Pairs[j].Before
|
||||
}
|
||||
return out.Pairs[i].After < out.Pairs[j].After
|
||||
if out.Pairs[i].After != out.Pairs[j].After {
|
||||
return out.Pairs[i].After < out.Pairs[j].After
|
||||
}
|
||||
return out.Pairs[i].Why < out.Pairs[j].Why
|
||||
})
|
||||
// Kahn's walk, the next always the first by repository and id among those with nothing before them.
|
||||
waiting := map[string]int{}
|
||||
@@ -779,89 +731,6 @@ func orderOf(members []orderMember, reach map[string]orderReach, edges []invento
|
||||
return out
|
||||
}
|
||||
|
||||
// orderRules is the rules in the order a pair names them when more than one says the same.
|
||||
var orderRules = []string{orderDeclared, orderBuiltBy, orderVersionSkew, orderEngineFirst}
|
||||
|
||||
// resolveClaims is the pairs the rules' claims come to (novox/hq ADR 0249): for each two members, the claims
|
||||
// of the highest precedence decide, and the claims they overrule are said on the pair. Claims of one rank
|
||||
// both ways are a contradiction: both pairs are kept, so the walk finds the cycle, and the contradiction
|
||||
// says how a person declares the order.
|
||||
func resolveClaims(claims []orderPair, byID map[string]orderMember) ([]orderPair, []orderContradiction) {
|
||||
byTwo := map[[2]string][]orderPair{}
|
||||
var keys [][2]string
|
||||
for _, c := range claims {
|
||||
k := [2]string{min(c.Before, c.After), max(c.Before, c.After)}
|
||||
if _, ok := byTwo[k]; !ok {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
byTwo[k] = append(byTwo[k], c)
|
||||
}
|
||||
slices.SortFunc(keys, func(a, b [2]string) int {
|
||||
if c := strings.Compare(a[0], b[0]); c != 0 {
|
||||
return c
|
||||
}
|
||||
return strings.Compare(a[1], b[1])
|
||||
})
|
||||
ruleIndex := func(why string) int { return slices.Index(orderRules, why) }
|
||||
var pairs []orderPair
|
||||
var contradictions []orderContradiction
|
||||
for _, k := range keys {
|
||||
cs := byTwo[k]
|
||||
slices.SortStableFunc(cs, func(a, b orderPair) int { return ruleIndex(a.Why) - ruleIndex(b.Why) })
|
||||
top := orderRank(cs[0].Why)
|
||||
var won []orderPair
|
||||
ways := map[string]bool{}
|
||||
for _, c := range cs {
|
||||
if orderRank(c.Why) == top {
|
||||
won = append(won, c)
|
||||
ways[c.Before] = true
|
||||
}
|
||||
}
|
||||
if len(ways) == 1 {
|
||||
p := orderPair{Before: won[0].Before, After: won[0].After, Why: won[0].Why}
|
||||
for _, c := range cs {
|
||||
if c.Before != p.Before && !slices.Contains(p.Over, c.Why) {
|
||||
p.Over = append(p.Over, c.Why)
|
||||
}
|
||||
}
|
||||
pairs = append(pairs, p)
|
||||
continue
|
||||
}
|
||||
// One rank both ways: no precedence resolves it.
|
||||
var first, second *orderPair
|
||||
var said []string
|
||||
for i := range won {
|
||||
c := won[i]
|
||||
if c.Before == k[0] && first == nil {
|
||||
first = &won[i]
|
||||
}
|
||||
if c.Before == k[1] && second == nil {
|
||||
second = &won[i]
|
||||
}
|
||||
said = append(said, fmt.Sprintf("%s puts %s first", c.Why, c.Before))
|
||||
}
|
||||
pairs = append(pairs, orderPair{Before: first.Before, After: first.After, Why: first.Why},
|
||||
orderPair{Before: second.Before, After: second.After, Why: second.Why})
|
||||
a, b := byID[k[0]], byID[k[1]]
|
||||
how := fmt.Sprintf("neither outranks the other: declare the order with a line `after: %s` in %s's "+
|
||||
"description, or `after: %s` in %s's", a.Repository, pullName(b), b.Repository, pullName(a))
|
||||
if top == orderRank(orderDeclared) {
|
||||
how = "each pull request declares it goes after the other: remove one of the `after:` lines"
|
||||
}
|
||||
contradictions = append(contradictions, orderContradiction{Members: []string{k[0], k[1]}, Rules: won,
|
||||
Said: fmt.Sprintf("%s and %s: %s; %s", k[0], k[1], strings.Join(said, ", "), how)})
|
||||
}
|
||||
return pairs, contradictions
|
||||
}
|
||||
|
||||
// pullName is a member as a person finds it: its pull request, or its id when no number was said.
|
||||
func pullName(m orderMember) string {
|
||||
if m.Number > 0 {
|
||||
return fmt.Sprintf("%s#%d", m.Repository, m.Number)
|
||||
}
|
||||
return m.ID
|
||||
}
|
||||
|
||||
// reachOfMembers is each member's reach, as the planner says it.
|
||||
func reachOfMembers(members []orderMember, entries []inventory.Entry, read map[string][]inventory.ReadRepository,
|
||||
edges []inventory.Edge) map[string]orderReach {
|
||||
@@ -959,7 +828,11 @@ func deliveryCommand(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return answer(orderOf(members, reachOfMembers(members, entries, read, edges), edges))
|
||||
changes, err := changesOf(ctx, changeSourcesOf(members))
|
||||
if err != nil {
|
||||
return fmt.Errorf("what the members say of their manifests cannot be read: %w", err)
|
||||
}
|
||||
return answer(orderOf(members, reachOfMembers(members, entries, read, edges), edges, changes))
|
||||
case "check":
|
||||
if *group == "" && len(members) == 1 {
|
||||
// One head, checked again as the forge's announcement would have it (a recheck): the controller's
|
||||
|
||||
Reference in New Issue
Block a user