| new file mode 100644 |
| index 000000000..4d33e8e3e |
| --- /dev/null |
| +++ b/internal/rolloutwords/words.go |
| @@ -0,0 +1,124 @@ |
| +// Package rolloutwords holds every member-facing phrase about a rollout: |
| +// the causes of an attempt (zero-downtime spec §6.3), the reasons a build |
| +// failed, the short states `carlos instances list` and the console show |
| +// (§9.1, §9.2), and the events a box writes to the platform log stream |
| +// (§9.4). One home, so the CLI, the console and the box never word one fact |
| +// two ways. Every string here was copy-reviewed; approved_copy_zdd_test.go |
| +// pins it against docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-copy.json. |
| +// |
| +// A leaf: it imports nothing of this repo's, so the rollout manager and the |
| +// activator can use it without an import cycle. |
| +package rolloutwords |
| + |
| +import "fmt" |
| + |
| +// causes are the attempt causes of spec §6.3, as a member reads them. |
| +var causes = map[string]string{ |
| + "release": "new release", // TODO-COPY zdd.cause.release |
| + "install": "installed by hand", // TODO-COPY zdd.cause.install |
| + "retry": "retry", // TODO-COPY zdd.cause.retry |
| + "restart": "carlos restart", // TODO-COPY zdd.cause.restart |
| + "config": "config change", // TODO-COPY zdd.cause.config |
| +} |
| + |
| +// Cause renders an attempt's cause. One this binary does not know is shown |
| +// as its own word, never dropped. |
| +func Cause(c string) string { |
| + if p, ok := causes[c]; ok { |
| + return p |
| + } |
| + return c |
| +} |
| + |
| +// reasons are the box's failure words, read after "because". |
| +var reasons = map[string]string{ |
| + "health": "it did not answer its health check within 30 seconds", // TODO-COPY zdd.reason.health |
| + "settle": "it stopped within 10 seconds of answering its health check", // TODO-COPY zdd.reason.settle |
| + "exit": "it exited before it was ready", // TODO-COPY zdd.reason.exit |
| + "staging": "the copy on the box did not match the release", // TODO-COPY zdd.reason.staging |
| + "reused": "this version name was shipped twice with different content, so ship it under a new name", // TODO-COPY zdd.reason.reused |
| +} |
| + |
| +// TODO-COPY zdd.reason.unknown |
| +const reasonUnknown = "it failed (%s)" |
| + |
| +// Reason renders a failure word. One this binary does not know still says |
| +// the build failed, and carries the box's own word for it. |
| +func Reason(r string) string { |
| + if p, ok := reasons[r]; ok { |
| + return p |
| + } |
| + return fmt.Sprintf(reasonUnknown, r) |
| +} |
| + |
| +// Event formats (spec §9.4), one per text function in internal/rollout/events.go |
| +// plus the bounce. TestVerbsAreWhereTheCallersPutThem pins each verb count. |
| +const ( |
| + // TODO-COPY zdd.event.swapping |
| + EventSwapping = "starting build %s beside the running one (%s)" |
| + // TODO-COPY zdd.event.swapped |
| + EventSwapped = "now serving build %s. The previous build finished its requests and stopped in %s" |
| + // TODO-COPY zdd.event.swap-failed |
| + EventSwapFailed = "build %s did not take over, because %s. Still serving build %s" |
| + // TODO-COPY zdd.event.hold-down |
| + 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" |
| + // TODO-COPY zdd.event.cohort-halted |
| + 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" |
| + // TODO-COPY zdd.event.fallback-served |
| + EventFallbackServed = "build %s failed to start, so the last good build, %s, is serving instead" |
| + // TODO-COPY zdd.event.config-pending |
| + EventConfigPending = "new config reached this box but is not in use yet, because %s. Trying again in %s" |
| + // TODO-COPY zdd.event.staging-refused |
| + EventStagingRefused = "build %s is not staged correctly on this box, so the box asked for it to be staged again" |
| + // TODO-COPY zdd.event.bounce-instance |
| + EventBounceInstance = "instance restarting (%s)" |
| + // TODO-COPY zdd.event.bounce-sidecar |
| + EventBounceSidecar = "sidecar restarting (%s)" |
| +) |
| + |
| +// States: the REASON column of `carlos instances list` (spec §9.1). |
| +const ( |
| + // TODO-COPY zdd.state.held |
| + StateHeld = "held: build %s failed on this box" |
| + // TODO-COPY zdd.state.rolling-out |
| + StateRollingOut = "rolling out" |
| + // TODO-COPY zdd.state.asleep |
| + StateAsleep = "starts %s on its next request" |
| + // TODO-COPY zdd.state.config-pending |
| + StateConfigPending = "config delivered, not active" |
| +) |
| + |
| +// unavailablePhrases are the member-facing reasons a box cannot run zero-downtime deploys, |
| +// keyed by the codes in status.RolloutReport.Unavailable. |
| +var unavailablePhrases = map[string]string{ |
| + "no-cgroup-delegation": "its box cannot run two builds side by side, so it restarts instances the old way", // TODO-COPY zdd.unavailable.declined |
| + "no-generation-control": "its box cannot run two builds side by side, so it restarts instances the old way", // TODO-COPY zdd.unavailable.declined |
| + "generations-off": "its box cannot run two builds side by side, so it restarts instances the old way", // TODO-COPY zdd.unavailable.declined |
| + "no-deploy-store": "its box cannot start instances: the deploy store is unavailable", // TODO-COPY zdd.unavailable.store |
| + "no-release-key": "its box cannot start instances: the release key is unavailable", // TODO-COPY zdd.unavailable.release-key |
| + "failure-book-unreadable": "its box cannot start instances: its record of failed builds cannot be read", // TODO-COPY zdd.unavailable.failure-book |
| +} |
| + |
| +// TODO-COPY zdd.unavailable.unknown |
| +const unavailableUnknown = "its box reports a rollout problem (%s)" |
| + |
| +// Unavailable is the member-facing phrase for a box's |
| +// RolloutReport.Unavailable code. An unknown code (a newer box) reads |
| +// through UnavailableUnknown with the code itself. |
| +func Unavailable(code string) string { |
| + if p, ok := unavailablePhrases[code]; ok { |
| + return p |
| + } |
| + return fmt.Sprintf(unavailableUnknown, code) |
| +} |
| + |
| +// UnavailableRefuses reports whether code is one under which the box |
| +// refuses every managed start (as opposed to running today's paths). |
| +func UnavailableRefuses(code string) bool { |
| + switch code { |
| + case "no-deploy-store", "no-release-key", "failure-book-unreadable": |
| + return true |
| + default: |
| + return false |
| + } |
| +} |