fix: rehearsal lessons — route-before-adopt, per-host live link, /data 711, ship keys binaries by target
Kass rehearsal VERIFIED on the flagship: api/library 200 at kass.99.81.104.219.sslip.io serving restored production data via ship -> promote canary/rehearsal -> adopt. The stable promotion bakes until 2026-08-05 (supply-chain hold working as designed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 files changed,
+33
−4
cli.go+11 −0infra/flagship/host-init.sh+3 −1infra/scripts/promote-kass-rehearsal.sh+10 −0infra/scripts/rehearse-kass.sh+6 −2infra/tito/host-init.sh+3 −1
diff --git a/cli.go b/cli.go| index 441b40f2a..089135fcf 100644 |
| --- a/cli.go |
| +++ b/cli.go |
| @@ -163,6 +163,7 @@ func runShip(args []string) { |
| app := fs.String("app", "", "app name") |
| kind := fs.String("kind", "binary", "binary | static") |
| ver := fs.String("version", "", "version (git short sha)") |
| + target := fs.String("target", "linux-arm64", "GOOS-GOARCH the binary was built for (binary kind, single file)") |
| fs.Parse(args) |
| if *app == "" || *ver == "" || fs.NArg() == 0 { |
| log.Fatal("usage: carlos ship -app <a> -version <sha> [-kind binary|static] <files-or-site-dir>") |
| @@ -185,7 +186,17 @@ func runShip(args []string) { |
| log.Fatal(err) |
| } |
| addFile("site.tar.gz", data) |
| + } else if fs.NArg() == 1 { |
| + // The adopt pass looks artifacts up by GOOS-GOARCH, so a binary's |
| + // artifact key is its target, not its build filename (learned live |
| + // 2026-08-01: a basename-keyed artifact is invisible to every host). |
| + data, err := os.ReadFile(fs.Arg(0)) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + addFile(*target, data) |
| } else { |
| + // Multiple files: pre-named per target (linux-arm64, linux-amd64, ...). |
| for _, path := range fs.Args() { |
| data, err := os.ReadFile(path) |
| if err != nil { |
diff --git a/infra/flagship/host-init.sh b/infra/flagship/host-init.sh| index 5d97c61f9..c5e9f194c 100644 |
| --- a/infra/flagship/host-init.sh |
| +++ b/infra/flagship/host-init.sh |
| @@ -8,7 +8,9 @@ set -euo pipefail |
| useradd --system --home /opt/carlos --shell /usr/sbin/nologin carlos || true |
| mkdir -p /data /opt/carlos/versions /opt/carlos/live /etc/carlos |
| chown -R carlos:carlos /data /opt/carlos |
| -chmod 0700 /data |
| +# 711: tenant service users (kass, ...) need traversal into their own |
| +# /data/<app> subdirs; contents stay private per-owner. |
| +chmod 0711 /data |
| # /run/carlos is tmpfs-backed; recreate on every boot. |
| cat >/etc/tmpfiles.d/carlos.conf <<'EOF' |
diff --git a/infra/scripts/promote-kass-rehearsal.sh b/infra/scripts/promote-kass-rehearsal.sh| new file mode 100755 |
| index 000000000..30936023c |
| --- /dev/null |
| +++ b/infra/scripts/promote-kass-rehearsal.sh |
| @@ -0,0 +1,10 @@ |
| +#!/bin/bash |
| +# 🤖 Promote kass aedcae3 to canary/rehearsal (zero bake hold) so the B5 |
| +# rehearsal can adopt immediately; the stable promotion stays baking until |
| +# 2026-08-05 for the real cutover, exactly as designed. |
| +set -euo pipefail |
| +carlos=${CARLOS_BIN:-./carlos} |
| +AWS_PROFILE=keymail AWS_REGION=eu-west-1 \ |
| +CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \ |
| +CARLOS_RELEASE_KEY=$(aws --profile keymail ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \ |
| + "$carlos" promote -app kass aedcae3-r2 canary/rehearsal |
diff --git a/infra/scripts/rehearse-kass.sh b/infra/scripts/rehearse-kass.sh| index 966375de6..b4801b2ff 100755 |
| --- a/infra/scripts/rehearse-kass.sh |
| +++ b/infra/scripts/rehearse-kass.sh |
| @@ -39,7 +39,7 @@ After=network-online.target |
| [Service] |
| User=kass |
| EnvironmentFile=/data/kass.env |
| -ExecStart=/opt/carlos/live/kass serve |
| +ExecStart=/opt/carlos/live/$HOST serve |
| Restart=always |
| NoNewPrivileges=yes |
| ProtectSystem=strict |
| @@ -48,9 +48,13 @@ PrivateTmp=yes |
| [Install] |
| WantedBy=multi-user.target |
| UNIT |
| +# Order matters: the route must exist BEFORE adopt, or the pass has |
| +# nothing to stage (learned live 2026-08-01). /data needs traversal for |
| +# the kass user. The live link is per-host by design. |
| +chmod 711 /data |
| export \$(grep -v '^#' /etc/carlos/host.env | xargs) |
| -/opt/carlos/carlos adopt |
| /opt/carlos/carlos add -app kass -kind instance -addr 127.0.0.1:8080 -channel stable $HOST |
| +/opt/carlos/carlos adopt |
| systemctl daemon-reload && systemctl restart kass |
| sleep 4 |
| systemctl is-active kass |
diff --git a/infra/tito/host-init.sh b/infra/tito/host-init.sh| index 6406cb6e7..ea0ea3e66 100644 |
| --- a/infra/tito/host-init.sh |
| +++ b/infra/tito/host-init.sh |
| @@ -7,7 +7,9 @@ set -euo pipefail |
| useradd --system --home /opt/carlos --shell /usr/sbin/nologin carlos || true |
| mkdir -p /data /opt/carlos/versions /opt/carlos/live /etc/carlos |
| chown -R carlos:carlos /data /opt/carlos |
| -chmod 0700 /data |
| +# 711: tenant service users need traversal into their own /data/<app> |
| +# subdirs; contents stay private per-owner. |
| +chmod 0711 /data |
| cat >/etc/tmpfiles.d/carlos.conf <<'EOF' |
| d /run/carlos 0755 carlos carlos - |