Gate a release plan's first machine and roll a failed build back there (hq ADR 0236)
A build that reported applied was sent everywhere; one that then did nothing, served no tools or broke its machine's word reached every machine. Now the first machine is judged by the component's health (the core's definitions, as doctor probes H-*, or a module's own) three times over two minutes within ten; a failing gate puts the previous build back there once, marks the build, and says it as a condition and an event. Upgrades roll out by default; the bus is a planned step; a module deleted at its source is not built (the public-acme plan failure).
This commit is contained in:
+125
-17
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -143,20 +144,17 @@ func isAre(n int) string {
|
||||
return "are"
|
||||
}
|
||||
|
||||
// upgradeCommand says what should happen when a module's current version moves.
|
||||
// upgradeCommand says what should happen when a module's current version moves (ADR 0162 §3, ADR
|
||||
// 0235): every module's policy and where it comes from; one module's; or a person's choice for one.
|
||||
func upgradeCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("upgrade", flag.ContinueOnError)
|
||||
together := set.Bool("together", false,
|
||||
"send every machine running it at once, instead of one after another")
|
||||
"send every machine running it at once, instead of one machine first")
|
||||
why := set.String("why", "", "why this choice — required for record; kept and said with the policy")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
return errors.New("upgrade <module> [roll-out|record] [--together]")
|
||||
}
|
||||
module := positionals[0]
|
||||
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,6 +162,27 @@ func upgradeCommand(ctx context.Context, args []string) error {
|
||||
defer open.Close()
|
||||
inv := open.inventory
|
||||
|
||||
if len(positionals) == 0 {
|
||||
all, err := inv.Upgrades(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(all))
|
||||
for n := range all {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
u := all[n]
|
||||
line := fmt.Sprintf("%-28s %-9s %s", n, u.Policy(), u.From)
|
||||
if u.Why != "" {
|
||||
line += ": " + u.Why
|
||||
}
|
||||
fmt.Println(line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
module := positionals[0]
|
||||
if len(positionals) == 1 {
|
||||
decision, err := inv.UpgradeOf(ctx, module)
|
||||
if err != nil {
|
||||
@@ -173,10 +192,10 @@ func upgradeCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var decision inventory.Upgrade
|
||||
decision := inventory.Upgrade{Why: strings.TrimSpace(*why), By: link.Caller()}
|
||||
switch positionals[1] {
|
||||
case "roll-out":
|
||||
decision = inventory.Upgrade{RollOut: true, Together: *together}
|
||||
case "roll-out", "roll":
|
||||
decision.RollOut, decision.Together = true, *together
|
||||
case "record":
|
||||
if *together {
|
||||
// Refused rather than ignored: --together only means anything for a roll-out, and
|
||||
@@ -184,28 +203,53 @@ func upgradeCommand(ctx context.Context, args []string) error {
|
||||
return errors.New("`--together` says how to roll out, so it cannot be given with " +
|
||||
"`record`, which is the choice not to")
|
||||
}
|
||||
decision = inventory.Upgrade{}
|
||||
if decision.Why == "" {
|
||||
return fmt.Errorf("holding %s back from every merge is a choice a person reads later: --why <text> "+
|
||||
"(novox/hq ADR 0235). Nothing was changed", module)
|
||||
}
|
||||
case "default":
|
||||
if err := inv.ClearUpgradeOf(ctx, module); err != nil {
|
||||
return err
|
||||
}
|
||||
decision, err := inv.UpgradeOf(ctx, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(sayUpgrade(module, decision))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("upgrade <module> roll-out|record — not %q", positionals[1])
|
||||
return fmt.Errorf("upgrade <module> roll-out|record|default — not %q", positionals[1])
|
||||
}
|
||||
if err := inv.SetUpgradeOf(ctx, module, decision); err != nil {
|
||||
return err
|
||||
}
|
||||
decision, err = inv.UpgradeOf(ctx, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(sayUpgrade(module, decision))
|
||||
return nil
|
||||
}
|
||||
|
||||
func sayUpgrade(module string, u inventory.Upgrade) string {
|
||||
from := " (" + u.From
|
||||
if u.By != "" {
|
||||
from += ", " + u.By
|
||||
}
|
||||
if u.Why != "" {
|
||||
from += ": " + u.Why
|
||||
}
|
||||
from += ")"
|
||||
if !u.RollOut {
|
||||
return fmt.Sprintf("when %s moves, the mesh records it and the machines running it are "+
|
||||
"reported as behind", module)
|
||||
"reported as behind until a person pushes them%s", module, from)
|
||||
}
|
||||
if u.Together {
|
||||
return fmt.Sprintf("when %s moves, every machine running it is sent the new version "+
|
||||
"together", module)
|
||||
"together%s", module, from)
|
||||
}
|
||||
return fmt.Sprintf("when %s moves, the machines running it are sent the new version one at "+
|
||||
"a time, stopping at the first that fails", module)
|
||||
return fmt.Sprintf("when %s moves, one machine running it is sent the new version first and judged at "+
|
||||
"the gate; the rest follow once it passes, and a build that fails is put back there%s", module, from)
|
||||
}
|
||||
|
||||
// Announceable is every build this mesh recorded, in the shape the builder announces one.
|
||||
@@ -310,6 +354,13 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
|
||||
}
|
||||
}
|
||||
touched := whatTheMergeTouched(from, entries, m)
|
||||
// **A module the merge deleted is not built** (novox/hq ADR 0235): its manifest is gone, so the build
|
||||
// seat finds nothing saying what it is, and the plan failed on it (`has no module.json at …`) with
|
||||
// every other module of its tier left unsent. It is forgotten where nothing holds it, said otherwise.
|
||||
touched, deleted := splitDeleted(touched, m)
|
||||
for _, e := range deleted {
|
||||
retireDeleted(ctx, inv, e, m)
|
||||
}
|
||||
for _, e := range touched {
|
||||
if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil {
|
||||
return notNow(err)
|
||||
@@ -465,7 +516,52 @@ func mergeCandidates(m link.SourceMoved, entries []inventory.Entry,
|
||||
func wouldMove(m link.SourceMoved, entries []inventory.Entry,
|
||||
read map[string][]inventory.ReadRepository) []inventory.Entry {
|
||||
from, _, _ := mergeCandidates(m, entries, read)
|
||||
return whatTheMergeTouched(from, entries, m)
|
||||
touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m), m)
|
||||
return touched
|
||||
}
|
||||
|
||||
// splitDeleted parts the modules a merge touched into those it changed and those whose manifest it
|
||||
// removed — deleted at their source (ADR 0235).
|
||||
func splitDeleted(touched []inventory.Entry, m link.SourceMoved) (kept, deleted []inventory.Entry) {
|
||||
removed := map[string]bool{}
|
||||
for _, p := range m.Removed {
|
||||
removed[strings.Trim(p, "/")] = true
|
||||
}
|
||||
for _, e := range touched {
|
||||
dir := strings.Trim(e.Source.Path, "/")
|
||||
manifest := moduleManifestFile
|
||||
if dir != "" {
|
||||
manifest = dir + "/" + manifest
|
||||
}
|
||||
if len(removed) > 0 && removed[manifest] {
|
||||
deleted = append(deleted, e)
|
||||
continue
|
||||
}
|
||||
kept = append(kept, e)
|
||||
}
|
||||
return kept, deleted
|
||||
}
|
||||
|
||||
// retireDeleted is what the mesh does with a module deleted at its source: its record says the merge
|
||||
// was looked at, so it is not acted on again; it is forgotten where nothing holds it; where a machine
|
||||
// runs it or the mesh holds something for it, that is said, and nothing is built.
|
||||
func retireDeleted(ctx context.Context, inv *inventory.Inventory, e inventory.Entry, m link.SourceMoved) {
|
||||
name := e.Manifest.Module
|
||||
if err := inv.SourceMoved(ctx, name, m.Commit); err != nil {
|
||||
fmt.Printf(" %s was deleted from %s/%s at %.8s, and that could not be recorded: %v\n", name, m.Owner, m.Repo, m.Commit, err)
|
||||
}
|
||||
err := inv.ForgetModule(ctx, name)
|
||||
switch {
|
||||
case err == nil:
|
||||
fmt.Printf(" %s was deleted from %s/%s at %.8s: forgotten, nothing built\n", name, m.Owner, m.Repo, m.Commit)
|
||||
case errors.Is(err, inventory.ErrStillAssigned) || errors.Is(err, inventory.ErrStillHolds):
|
||||
fmt.Printf(" %s was deleted from %s/%s at %.8s and is not built; the mesh still runs or holds it, so it is "+
|
||||
"kept until a person unassigns it and `module forget %s`: %v\n", name, m.Owner, m.Repo, m.Commit, name,
|
||||
firstLine(err.Error()))
|
||||
default:
|
||||
fmt.Printf(" %s was deleted from %s/%s at %.8s and is not built; forgetting it failed: %v\n", name, m.Owner,
|
||||
m.Repo, m.Commit, err)
|
||||
}
|
||||
}
|
||||
|
||||
// sourceIs is whether a recorded source is the repository and branch a merge announced. A source on
|
||||
@@ -749,3 +845,15 @@ func dependentsOf(moved, entries []inventory.Entry, against map[string][]string)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// moduleManifestFile is the file that says what a module is, in its directory (the build seat's
|
||||
// ManifestName).
|
||||
const moduleManifestFile = "module.json"
|
||||
|
||||
// deletedAtSource is whether a build failed because its module's manifest is not at its source any
|
||||
// more — the build seat's own words (internal/builder) — which a merge that deleted the module causes
|
||||
// when its announcer did not say which files went (ADR 0235). Such a module is not a failure of the plan.
|
||||
func deletedAtSource(failed string) bool {
|
||||
return strings.Contains(failed, "has no "+moduleManifestFile+" at ") &&
|
||||
strings.Contains(failed, "so there is nothing saying what it is")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user