| 1 | // Package rolloutwords holds every member-facing phrase about a rollout: |
| 2 | // the causes of an attempt (zero-downtime spec §6.3), the reasons a build |
| 3 | // failed, the short states `carlos instances list` and the console show |
| 4 | // (§9.1, §9.2), and the events a box writes to the platform log stream |
| 5 | // (§9.4). One home, so the CLI, the console and the box never word one fact |
| 6 | // two ways. Every string here is approved copy, pinned by the zero-downtime |
| 7 | // copy record (docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-copy.json). |
| 8 | // |
| 9 | // A leaf: it imports nothing of this repo's, so the rollout manager and the |
| 10 | // activator can use it without an import cycle. |
| 11 | package rolloutwords |
| 12 | |
| 13 | import "fmt" |
| 14 | |
| 15 | // causes are the attempt causes of spec §6.3, as a member reads them. |
| 16 | var causes = map[string]string{ |
| 17 | "release": "new release", |
| 18 | "install": "installed by hand", |
| 19 | "retry": "retry", |
| 20 | "restart": "carlos restart", |
| 21 | "config": "config change", |
| 22 | } |
| 23 | |
| 24 | // Cause renders an attempt's cause. One this binary does not know is shown |
| 25 | // as its own word, never dropped. |
| 26 | func Cause(c string) string { |
| 27 | if p, ok := causes[c]; ok { |
| 28 | return p |
| 29 | } |
| 30 | return c |
| 31 | } |
| 32 | |
| 33 | // reasons are the box's failure words, read after "because". |
| 34 | var reasons = map[string]string{ |
| 35 | "health": "it did not answer its health check within 30 seconds", |
| 36 | "settle": "it stopped within 10 seconds of answering its health check", |
| 37 | "exit": "it exited before it was ready", |
| 38 | "staging": "the copy on the box did not match the release", |
| 39 | "reused": "this version name was shipped twice with different content, so ship it under a new name", |
| 40 | } |
| 41 | |
| 42 | const reasonUnknown = "it failed (%s)" |
| 43 | |
| 44 | // Reason renders a failure word. One this binary does not know still says |
| 45 | // the build failed, and carries the box's own word for it. |
| 46 | func Reason(r string) string { |
| 47 | if p, ok := reasons[r]; ok { |
| 48 | return p |
| 49 | } |
| 50 | return fmt.Sprintf(reasonUnknown, r) |
| 51 | } |
| 52 | |
| 53 | // Event formats (spec §9.4), one per text function in internal/rollout/events.go |
| 54 | // plus the bounce. TestVerbsAreWhereTheCallersPutThem pins each verb count. |
| 55 | const ( |
| 56 | // Emitted for every attempt, overlap, hold and a socketless worker's |
| 57 | // stop-then-start alike (rollout's execute and executeWorker), so it |
| 58 | // says nothing about the mode (Astra plan 6 R1 #8). |
| 59 | EventSwapping = "starting build %s (%s)" |
| 60 | EventSwapped = "now serving build %s. The previous build finished its requests and stopped in %s" |
| 61 | EventSwapFailed = "build %s did not take over, because %s. Still serving build %s" |
| 62 | EventHoldDown = "build %s failed and nothing is serving. This app swaps in hold mode, so nothing falls back by itself. Roll back with carlos rollback" |
| 63 | EventCohortHalted = "rollout of build %s stopped on box %s, because %s. Instances on that box that have not served it stay on their last good build" |
| 64 | EventFallbackServed = "build %s failed to start, so the last good build, %s, is serving instead" |
| 65 | EventConfigPending = "new config reached this box but is not in use yet, because %s. Trying again in %s" |
| 66 | EventStagingRefused = "build %s is not staged correctly on this box, so the box asked for it to be staged again" |
| 67 | EventBounceInstance = "instance restarting (%s)" |
| 68 | EventBounceSidecar = "sidecar restarting (%s)" |
| 69 | ) |
| 70 | |
| 71 | // States: the REASON column of `carlos instances list` (spec §9.1). |
| 72 | const ( |
| 73 | StateHeld = "held: build %s failed on this box" |
| 74 | StateRollingOut = "rolling out" |
| 75 | StateAsleep = "starts %s on its next request" |
| 76 | StateConfigPending = "config delivered, not active" |
| 77 | ) |
| 78 | |
| 79 | const unavailableDeclined = "its box cannot run two builds side by side, so it restarts instances the old way" |
| 80 | |
| 81 | // unavailablePhrases are the member-facing reasons a box cannot run zero-downtime deploys, |
| 82 | // keyed by the codes in status.RolloutReport.Unavailable. |
| 83 | var unavailablePhrases = map[string]string{ |
| 84 | "no-cgroup-delegation": unavailableDeclined, |
| 85 | "no-generation-control": unavailableDeclined, |
| 86 | "generations-off": unavailableDeclined, |
| 87 | "host-claims-on": unavailableDeclined, |
| 88 | "cgroup-placement-failed": unavailableDeclined, |
| 89 | "no-deploy-store": "its box cannot start instances: the deploy store is unavailable", |
| 90 | "no-release-key": "its box cannot start instances: the release key is unavailable", |
| 91 | "failure-book-unreadable": "its box cannot start instances: its record of failed builds cannot be read", |
| 92 | } |
| 93 | |
| 94 | const unavailableUnknown = "its box reports a rollout problem (%s)" |
| 95 | |
| 96 | // Unavailable is the member-facing phrase for a box's |
| 97 | // RolloutReport.Unavailable code. An unknown code (a newer box) reads |
| 98 | // through unavailableUnknown with the code itself. |
| 99 | func Unavailable(code string) string { |
| 100 | if p, ok := unavailablePhrases[code]; ok { |
| 101 | return p |
| 102 | } |
| 103 | return fmt.Sprintf(unavailableUnknown, code) |
| 104 | } |
| 105 | |
| 106 | // UnavailableDeclines reports whether code is one under which the box |
| 107 | // declined CARLOS_SWAP=overlap and runs today's stop-then-start paths. An |
| 108 | // unknown code (a newer box) is not: a reader must not assume its instances |
| 109 | // are safe to wake. |
| 110 | func UnavailableDeclines(code string) bool { |
| 111 | switch code { |
| 112 | case "no-cgroup-delegation", "no-generation-control", "generations-off", "host-claims-on", "cgroup-placement-failed": |
| 113 | return true |
| 114 | default: |
| 115 | return false |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // UnavailableRefuses reports whether code is one under which the box |
| 120 | // refuses every managed start (as opposed to running today's paths). |
| 121 | func UnavailableRefuses(code string) bool { |
| 122 | switch code { |
| 123 | case "no-deploy-store", "no-release-key", "failure-book-unreadable": |
| 124 | return true |
| 125 | default: |
| 126 | return false |
| 127 | } |
| 128 | } |
| 129 | |