max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
11 files changed,
+315 insertions,
-22 deletions
| @@ -1056,7 +1056,7 @@ | |||
| 1056 | 1056 | ||
| 1057 | 1057 | [[package]] | |
| 1058 | 1058 | name = "magicmirror" | |
| 1059 | - | version = "0.1.0" | |
| 1059 | + | version = "0.1.1" | |
| 1060 | 1060 | dependencies = [ | |
| 1061 | 1061 | "anyhow", | |
| 1062 | 1062 | "chrono", | |
| @@ -2902,10 +2902,6 @@ | |||
| 2902 | 2902 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2903 | 2903 | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" | |
| 2904 | 2904 | ||
| 2905 | - | [[patch.unused]] | |
| 2906 | - | name = "docengine" | |
| 2907 | - | version = "0.7.0" | |
| 2908 | - | ||
| 2909 | 2905 | [[patch.unused]] | |
| 2910 | 2906 | name = "synckit-client" | |
| 2911 | 2907 | version = "0.8.1" | |
| @@ -2914,10 +2910,6 @@ | |||
| 2914 | 2910 | name = "synckit-config" | |
| 2915 | 2911 | version = "0.2.0" | |
| 2916 | 2912 | ||
| 2917 | - | [[patch.unused]] | |
| 2918 | - | name = "quasi-type" | |
| 2919 | - | version = "0.1.0" | |
| 2920 | - | ||
| 2921 | 2913 | [[patch.unused]] | |
| 2922 | 2914 | name = "quasi-axum" | |
| 2923 | 2915 | version = "0.56.0" | |
| @@ -2965,3 +2957,11 @@ | |||
| 2965 | 2957 | [[patch.unused]] | |
| 2966 | 2958 | name = "tagtree" | |
| 2967 | 2959 | version = "0.4.1" | |
| 2960 | + | ||
| 2961 | + | [[patch.unused]] | |
| 2962 | + | name = "docengine" | |
| 2963 | + | version = "0.7.0" | |
| 2964 | + | ||
| 2965 | + | [[patch.unused]] | |
| 2966 | + | name = "quasi-type" | |
| 2967 | + | version = "0.1.0" |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "magicmirror" | |
| 3 | - | version = "0.1.0" | |
| 3 | + | version = "0.1.1" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license = "MIT" | |
| 6 | 6 | description = "One terminal surface over every operator daemon that emits an ops-status payload. Tabs per source plus a worst-first rollup." |
| @@ -1931,7 +1931,7 @@ | |||
| 1931 | 1931 | ||
| 1932 | 1932 | [[package]] | |
| 1933 | 1933 | name = "mnw-cli" | |
| 1934 | - | version = "0.1.1" | |
| 1934 | + | version = "0.1.2" | |
| 1935 | 1935 | dependencies = [ | |
| 1936 | 1936 | "anyhow", | |
| 1937 | 1937 | "bytes", | |
| @@ -4761,6 +4761,10 @@ | |||
| 4761 | 4761 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4762 | 4762 | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | |
| 4763 | 4763 | ||
| 4764 | + | [[patch.unused]] | |
| 4765 | + | name = "docengine" | |
| 4766 | + | version = "0.7.0" | |
| 4767 | + | ||
| 4764 | 4768 | [[patch.unused]] | |
| 4765 | 4769 | name = "quasi-axum" | |
| 4766 | 4770 | version = "0.56.0" | |
| @@ -4797,14 +4801,6 @@ | |||
| 4797 | 4801 | name = "quasi-webview" | |
| 4798 | 4802 | version = "0.56.0" | |
| 4799 | 4803 | ||
| 4800 | - | [[patch.unused]] | |
| 4801 | - | name = "synckit-config" | |
| 4802 | - | version = "0.2.0" | |
| 4803 | - | ||
| 4804 | - | [[patch.unused]] | |
| 4805 | - | name = "docengine" | |
| 4806 | - | version = "0.7.0" | |
| 4807 | - | ||
| 4808 | 4804 | [[patch.unused]] | |
| 4809 | 4805 | name = "kberg" | |
| 4810 | 4806 | version = "0.1.0" | |
| @@ -4824,3 +4820,7 @@ | |||
| 4824 | 4820 | [[patch.unused]] | |
| 4825 | 4821 | name = "quasi-type" | |
| 4826 | 4822 | version = "0.1.0" | |
| 4823 | + | ||
| 4824 | + | [[patch.unused]] | |
| 4825 | + | name = "synckit-config" | |
| 4826 | + | version = "0.2.0" |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "mnw-cli" | |
| 3 | - | version = "0.1.1" | |
| 3 | + | version = "0.1.2" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license = "LicenseRef-PolyForm-Noncommercial-1.0.0" | |
| 6 | 6 |
| @@ -46,6 +46,28 @@ | |||
| 46 | 46 | const TICK: Duration = Duration::from_millis(500); | |
| 47 | 47 | ||
| 48 | 48 | fn main() -> Result<()> { | |
| 49 | + | // `--version`, answered before the terminal is taken over and before any | |
| 50 | + | // config is read. | |
| 51 | + | // | |
| 52 | + | // Not decoration. This binary is placed on astra by hand and drawn onto | |
| 53 | + | // tty1, and until now there was no way to ask a running one what it was: | |
| 54 | + | // the only handle was the mtime of the file, which is how the sibling | |
| 55 | + | // services on that host came to be four months stale without anyone | |
| 56 | + | // noticing. Bento's release recipes also assert | |
| 57 | + | // `<binary> --version | grep -qw <version>` to prove the checkout and the | |
| 58 | + | // release agree. Infra `e6acf532`. | |
| 59 | + | // | |
| 60 | + | // Hand-rolled rather than reached for clap: this crate takes no argument | |
| 61 | + | // parser at all, and a dependency for one string would be the wrong trade. | |
| 62 | + | let argv: Vec<String> = std::env::args().collect(); | |
| 63 | + | if matches!( | |
| 64 | + | argv.get(1).map(String::as_str), | |
| 65 | + | Some("--version" | "-V" | "version") | |
| 66 | + | ) { | |
| 67 | + | println!("magicmirror {}", env!("CARGO_PKG_VERSION")); | |
| 68 | + | return Ok(()); | |
| 69 | + | } | |
| 70 | + | ||
| 49 | 71 | // Before any TLS work: reqwest is built `rustls-no-provider`, so without a | |
| 50 | 72 | // process default every client build would fail at the first poll. | |
| 51 | 73 | tls::install_crypto_provider(); |
| @@ -9,7 +9,14 @@ | |||
| 9 | 9 | User=mnw-cli | |
| 10 | 10 | Group=mnw-cli | |
| 11 | 11 | WorkingDirectory=/opt/mnw-cli | |
| 12 | - | ExecStart=/opt/mnw-cli/mnw-cli | |
| 12 | + | # /usr/local/bin, not /opt/mnw-cli, since 2026-08-23. The binary is delivered by | |
| 13 | + | # Bento now, and install-service.sh refuses any dst outside /usr/local/bin -- | |
| 14 | + | # that guard is the only thing bounding a NOPASSWD sudo grant, so the binary | |
| 15 | + | # moves rather than the guard widening. Same call wam made on 2026-08-15. | |
| 16 | + | # | |
| 17 | + | # WorkingDirectory stays /opt/mnw-cli deliberately: that is this service's state | |
| 18 | + | # directory (.env, HOME, ReadWritePaths), not where the binary lives. | |
| 19 | + | ExecStart=/usr/local/bin/mnw-cli | |
| 13 | 20 | Restart=always | |
| 14 | 21 | RestartSec=5 | |
| 15 | 22 |
| @@ -32,9 +32,31 @@ | |||
| 32 | 32 | ||
| 33 | 33 | #[tokio::main] | |
| 34 | 34 | async fn main() -> anyhow::Result<()> { | |
| 35 | + | let argv: Vec<String> = std::env::args().collect(); | |
| 36 | + | ||
| 37 | + | // `--version`, answered before anything else and before any config is read. | |
| 38 | + | // | |
| 39 | + | // Not decoration. This binary was placed on astra by hand and sat four | |
| 40 | + | // months stale (an April build against a repo that had moved on), and the | |
| 41 | + | // reason nobody noticed is that there was no way to ask a running one what | |
| 42 | + | // it was: the only handle was the mtime of the file. A service that cannot | |
| 43 | + | // say its own version cannot be audited, and Bento's release recipes assert | |
| 44 | + | // `<binary> --version | grep -qw <version>` to prove the checkout and the | |
| 45 | + | // release agree. Infra `e6acf532`. | |
| 46 | + | // | |
| 47 | + | // Hand-rolled rather than reached for clap: this crate takes no argument | |
| 48 | + | // parser at all (`ota` routes on `argv[1]` a few lines down), and a | |
| 49 | + | // dependency for one string would be the wrong trade. | |
| 50 | + | if matches!( | |
| 51 | + | argv.get(1).map(String::as_str), | |
| 52 | + | Some("--version" | "-V" | "version") | |
| 53 | + | ) { | |
| 54 | + | println!("mnw-cli {}", env!("CARGO_PKG_VERSION")); | |
| 55 | + | return Ok(()); | |
| 56 | + | } | |
| 57 | + | ||
| 35 | 58 | // One-shot operator subcommand: `mnw-cli ota publish ...`. Routed before the | |
| 36 | 59 | // SSH daemon boots so the same binary doubles as the OTA publisher. | |
| 37 | - | let argv: Vec<String> = std::env::args().collect(); | |
| 38 | 60 | if argv.get(1).map(String::as_str) == Some("ota") { | |
| 39 | 61 | return ota::run(&argv[2..]).await; | |
| 40 | 62 | } |
| @@ -1,0 +1,47 @@ | |||
| 1 | + | # How Bento releases magicmirror. Lives here rather than in the daemon's config | |
| 2 | + | # so it is versioned with the code it describes. | |
| 3 | + | # | |
| 4 | + | # A service, not an app or a library: magicmirror is run rather than distributed. | |
| 5 | + | # There is no bundle for a user to download and no crate to publish. | |
| 6 | + | # | |
| 7 | + | # WHY THIS FILE EXISTS. astra's copy was a binary somebody had placed at | |
| 8 | + | # ~/.local/bin/magicmirror by hand, with no release history, no rollback and no | |
| 9 | + | # way to ask it what it was -- the only handle was the file's mtime. It was the | |
| 10 | + | # least stale of the three services on that host in that state, which is not the | |
| 11 | + | # same as being watched. Infra `e6acf532`. | |
| 12 | + | kind = "service" | |
| 13 | + | ||
| 14 | + | # aarch64 only. This runs on exactly one machine: it is drawn onto astra's tty1 | |
| 15 | + | # as an always-on wall panel, and there is no second instance anywhere. | |
| 16 | + | targets = ["linux/aarch64"] | |
| 17 | + | ||
| 18 | + | version_path = "Cargo.toml" | |
| 19 | + | ||
| 20 | + | # MNW is one .git over several separately-versioned products, so the default | |
| 21 | + | # `v{version}` names no product in particular here. | |
| 22 | + | tag_format = "magicmirror-v{version}" | |
| 23 | + | ||
| 24 | + | # One target, so inert either way. Stated rather than omitted so the answer does | |
| 25 | + | # not have to be reasoned out if a second is ever added. | |
| 26 | + | require_all_targets = true | |
| 27 | + | ||
| 28 | + | # CEREMONY, and this is the cheapest rung on purpose. A bad deploy here costs a | |
| 29 | + | # monitoring panel on a console going blank. Nobody's money, nobody's morning, | |
| 30 | + | # and no data. So it installs itself through Bento the way wam does rather than | |
| 31 | + | # paying for Sando's tiers, and the rollback is the installer's `<dst>.prev`. | |
| 32 | + | # See wiki `deploy-ceremony-tiers`. | |
| 33 | + | # | |
| 34 | + | # install_path is /usr/local/bin/magicmirror, which is a MOVE: the running | |
| 35 | + | # binary was at ~/.local/bin/magicmirror. install-service.sh refuses any dst | |
| 36 | + | # outside /usr/local/bin, and that guard is the only thing bounding a NOPASSWD | |
| 37 | + | # sudo grant, so the binary moves rather than the guard widening -- the same | |
| 38 | + | # call wam made on 2026-08-15. The unit's ExecStart moves with it. | |
| 39 | + | # | |
| 40 | + | # No `health_url`: this is a TUI drawn on a console, not a server, so there is | |
| 41 | + | # no port to probe and no status code to read. The recipe asserts the unit is | |
| 42 | + | # active, which is the whole of what "up" means for this one. | |
| 43 | + | [[deploy]] | |
| 44 | + | target = "linux/aarch64" | |
| 45 | + | host = "astra" | |
| 46 | + | install_path = "/usr/local/bin/magicmirror" | |
| 47 | + | service = "magicmirror-panel.service" |
| @@ -1,0 +1,56 @@ | |||
| 1 | + | // magicmirror: Linux release recipe for Bento (aarch64 on astra). | |
| 2 | + | // | |
| 3 | + | // Ends at `deploy`, not `collect`: magicmirror installs itself rather than | |
| 4 | + | // handing off to Sando. That is a judgement about blast radius, not a house | |
| 5 | + | // style -- a bad deploy here blanks a monitoring panel on a console. The | |
| 6 | + | // cheapest rung, honestly taken. See wiki `deploy-ceremony-tiers`. | |
| 7 | + | // | |
| 8 | + | // No host is named anywhere in here: build_host() resolves to whichever native | |
| 9 | + | // machine the topology assigns aarch64, and deploy() to whichever machine | |
| 10 | + | // bento.toml says runs it. They happen to be the same box; the recipe should | |
| 11 | + | // not know that. | |
| 12 | + | ||
| 13 | + | let h = build_host(); | |
| 14 | + | let v = version(); | |
| 15 | + | let r = repo(); | |
| 16 | + | ||
| 17 | + | step("checkout"); | |
| 18 | + | let sha = checkout_sha(h); | |
| 19 | + | log("pinned " + h + " to magicmirror-v" + v + " @ " + sha); | |
| 20 | + | ||
| 21 | + | step("prebuild"); | |
| 22 | + | sh_ok(h, "cd " + r + " && cargo clippy --all-targets " + feature_flags() + " -- -D warnings"); | |
| 23 | + | sh_ok(h, "cd " + r + " && cargo test " + feature_flags()); | |
| 24 | + | ||
| 25 | + | step("build"); | |
| 26 | + | sh_ok(h, "cd " + r + " && cargo build --release " + feature_flags()); | |
| 27 | + | let binary = resolve_artifact(h, r + "/target/release/magicmirror"); | |
| 28 | + | ||
| 29 | + | step("verify"); | |
| 30 | + | log(glibc_check(binary)); | |
| 31 | + | ||
| 32 | + | // The version about to ship is the version in the tag. This assertion is the | |
| 33 | + | // reason `--version` exists on this binary at all: it was added 2026-08-23 with | |
| 34 | + | // this recipe, and before it the only way to ask a running magicmirror what it | |
| 35 | + | // was was to stat the file. | |
| 36 | + | sh_ok(h, binary + " --version | grep -qw " + v); | |
| 37 | + | ||
| 38 | + | step("deploy"); | |
| 39 | + | // Stages under /var/tmp/bento-deploy/magicmirror/ and calls the privileged | |
| 40 | + | // installer, which keeps the outgoing binary as <dst>.prev, installs | |
| 41 | + | // atomically, and restarts the unit. | |
| 42 | + | log(deploy(binary)); | |
| 43 | + | ||
| 44 | + | // Health assertions live inside the deploy step rather than a step of their | |
| 45 | + | // own: a "health" step would abort the run AFTER the install has happened, | |
| 46 | + | // leaving the binary live and the release reading failed. | |
| 47 | + | // | |
| 48 | + | // This is a TUI on a console, so there is no port to probe. The unit being | |
| 49 | + | // active is the whole of what "up" means here -- and it is not a formality: the | |
| 50 | + | // binary reads only ~/.config/magicmirror/ with no fallback to the old | |
| 51 | + | // ops-viewer path, so a config that did not move with a rename is exactly the | |
| 52 | + | // failure that leaves the unit dead and the screen dark. | |
| 53 | + | sh_ok(deploy_host(), "test -x " + install_path()); | |
| 54 | + | sh_ok(deploy_host(), "systemctl is-active --quiet magicmirror-panel.service"); | |
| 55 | + | ||
| 56 | + | log("magicmirror " + v + " (" + target() + ") built on " + h + " and live on " + deploy_host()); |
| @@ -1,0 +1,59 @@ | |||
| 1 | + | # How Bento releases mnw-cli. Lives here rather than in the daemon's config so | |
| 2 | + | # it is versioned with the code it describes. | |
| 3 | + | # | |
| 4 | + | # A service, not an app or a library: mnw-cli is run rather than distributed. | |
| 5 | + | # There is no bundle for a user to download and no crate to publish. | |
| 6 | + | # | |
| 7 | + | # WHY THIS FILE EXISTS AT ALL, which is the whole point of it. Until 2026-08-23 | |
| 8 | + | # astra's mnw-cli was a binary somebody had placed there by hand in April, with | |
| 9 | + | # no release history, no rollback and no way to ask it what it was -- the only | |
| 10 | + | # handle was the file's mtime. It was four months old against a source tree that | |
| 11 | + | # had moved on, and nothing could have noticed, because a service outside the | |
| 12 | + | # pipeline has no release history to be stale against. Infra `e6acf532`. | |
| 13 | + | kind = "service" | |
| 14 | + | ||
| 15 | + | # aarch64 only, and the asymmetry is deliberate rather than an omission. | |
| 16 | + | # | |
| 17 | + | # The Hetzner box already gets this binary, but it gets it INSIDE the MNW server | |
| 18 | + | # release: Sando ships it as a companion to `/opt/mnw/current/companions/mnw-cli`, | |
| 19 | + | # so prod's copy is pipelined already and building a second x86_64 one here would | |
| 20 | + | # be two delivery stories for one binary on one machine. astra's copy is the one | |
| 21 | + | # with no pipeline, and astra is aarch64. | |
| 22 | + | # | |
| 23 | + | # If prod ever stops carrying it as a companion, add `linux/x86_64` here and a | |
| 24 | + | # second `[[deploy]]` -- do not hand-place it. | |
| 25 | + | targets = ["linux/aarch64"] | |
| 26 | + | ||
| 27 | + | version_path = "Cargo.toml" | |
| 28 | + | ||
| 29 | + | # MNW is one .git over the server, sando, multithreaded, pom, wam, magicmirror | |
| 30 | + | # and mnw-cli, each versioned separately, so the default `v{version}` names no | |
| 31 | + | # product in particular here. | |
| 32 | + | tag_format = "mnw-cli-v{version}" | |
| 33 | + | ||
| 34 | + | # One target, so this is inert either way. Stated rather than omitted so the | |
| 35 | + | # answer does not have to be worked out from first principles if a second | |
| 36 | + | # target is ever added. | |
| 37 | + | require_all_targets = true | |
| 38 | + | ||
| 39 | + | # The service host. A deploy destination is deliberately NOT a `[[host]]` in the | |
| 40 | + | # daemon config: a build host is granted build/package, a service host | |
| 41 | + | # deploy/restart, and neither is granted the other's. | |
| 42 | + | # | |
| 43 | + | # install_path is /usr/local/bin/mnw-cli, which is a MOVE: the running binary | |
| 44 | + | # was at /opt/mnw-cli/mnw-cli, placed there by hand in April. install-service.sh | |
| 45 | + | # refuses any dst outside /usr/local/bin, and that guard is the only thing | |
| 46 | + | # bounding a NOPASSWD sudo grant, so the binary moves rather than the guard | |
| 47 | + | # widening. That is the same call wam made on 2026-08-15 and for the same | |
| 48 | + | # reason. The unit's ExecStart moves with it; WorkingDirectory stays | |
| 49 | + | # /opt/mnw-cli, which is its state directory (.env, HOME, ReadWritePaths) and | |
| 50 | + | # not where the binary lives. | |
| 51 | + | # | |
| 52 | + | # No `health_url`: mnw-cli speaks SSH, not HTTP. The recipe asserts the unit is | |
| 53 | + | # active and that the port answers with an SSH banner instead, which is the same | |
| 54 | + | # question in this service's own vocabulary. | |
| 55 | + | [[deploy]] | |
| 56 | + | target = "linux/aarch64" | |
| 57 | + | host = "astra" | |
| 58 | + | install_path = "/usr/local/bin/mnw-cli" | |
| 59 | + | service = "mnw-cli.service" |
| @@ -1,0 +1,80 @@ | |||
| 1 | + | // mnw-cli: Linux release recipe for Bento (aarch64 on astra). | |
| 2 | + | // | |
| 3 | + | // Ends at `deploy`, not `collect`. mnw-cli installs itself rather than handing | |
| 4 | + | // off to Sando, and that is a judgement about blast radius rather than a house | |
| 5 | + | // style: a bad deploy here stops git-over-SSH to the mirrors on astra. Nothing | |
| 6 | + | // user-facing, and the mirrors are the redundancy story rather than the serving | |
| 7 | + | // one, so it earns a real install path with a rollback and not Sando's tiers. | |
| 8 | + | // Same rung as wam, one below pom. See wiki `deploy-ceremony-tiers`. | |
| 9 | + | // | |
| 10 | + | // One arch, one host, and no host is named anywhere in here: build_host() | |
| 11 | + | // resolves to whichever native machine the topology assigns aarch64, and | |
| 12 | + | // deploy() to whichever machine bento.toml says runs it. | |
| 13 | + | ||
| 14 | + | let h = build_host(); | |
| 15 | + | let v = version(); | |
| 16 | + | let r = repo(); | |
| 17 | + | ||
| 18 | + | step("checkout"); | |
| 19 | + | // Pin to the release tag, not whatever main is at pull time. The binary that | |
| 20 | + | // ships has to be the binary the tag names, or the version it reports below is | |
| 21 | + | // a claim about a commit nobody built. | |
| 22 | + | let sha = checkout_sha(h); | |
| 23 | + | log("pinned " + h + " to mnw-cli-v" + v + " @ " + sha); | |
| 24 | + | ||
| 25 | + | step("prebuild"); | |
| 26 | + | sh_ok(h, "cd " + r + " && cargo clippy --all-targets " + feature_flags() + " -- -D warnings"); | |
| 27 | + | sh_ok(h, "cd " + r + " && cargo test " + feature_flags()); | |
| 28 | + | ||
| 29 | + | step("build"); | |
| 30 | + | sh_ok(h, "cd " + r + " && cargo build --release " + feature_flags()); | |
| 31 | + | let binary = resolve_artifact(h, r + "/target/release/mnw-cli"); | |
| 32 | + | ||
| 33 | + | step("verify"); | |
| 34 | + | // The build host must not produce a binary the service host's glibc is too old | |
| 35 | + | // to exec. Answerable here because this app keeps its `[[deploy]]` entry: the | |
| 36 | + | // check needs to know which machine runs the binary, and that entry is where | |
| 37 | + | // that is written. | |
| 38 | + | log(glibc_check(binary)); | |
| 39 | + | ||
| 40 | + | // The version about to ship is the version in the tag. A binary reporting | |
| 41 | + | // something else means the checkout and the release disagree. | |
| 42 | + | // | |
| 43 | + | // This assertion is the reason `--version` exists on this binary at all. It was | |
| 44 | + | // added 2026-08-23 with this recipe: before it, the only way to ask a running | |
| 45 | + | // mnw-cli what it was was to stat the file, which is exactly how astra's copy | |
| 46 | + | // sat four months stale without anyone noticing. | |
| 47 | + | sh_ok(h, binary + " --version | grep -qw " + v); | |
| 48 | + | ||
| 49 | + | step("deploy"); | |
| 50 | + | // Stages under /var/tmp/bento-deploy/mnw-cli/ and calls the privileged | |
| 51 | + | // installer, which keeps the outgoing binary as <dst>.prev, installs | |
| 52 | + | // atomically, and restarts the unit. That .prev file is the whole rollback | |
| 53 | + | // story, and it is proportionate: putting it back and restarting is one | |
| 54 | + | // command. | |
| 55 | + | log(deploy(binary)); | |
| 56 | + | ||
| 57 | + | // Health assertions live inside the deploy step rather than a step of their | |
| 58 | + | // own. `step()` names one of a fixed set the daemon knows, and a "health" step | |
| 59 | + | // would abort the run AFTER the install has already happened -- the binary live | |
| 60 | + | // and the release reading failed, which is the worst of both. Assertions about | |
| 61 | + | // the running service belong to the step that started it. | |
| 62 | + | // | |
| 63 | + | // What "healthy" means is the service's business. mnw-cli speaks SSH rather | |
| 64 | + | // than HTTP, so there is no status code to read: the proof is that the unit is | |
| 65 | + | // active and that the port answers with an SSH identification string. A dead | |
| 66 | + | // binary satisfies neither, and a binary that starts and then panics satisfies | |
| 67 | + | // the first only, which is why both are here. | |
| 68 | + | sh_ok(deploy_host(), "test -x " + install_path()); | |
| 69 | + | sh_ok(deploy_host(), "systemctl is-active --quiet mnw-cli.service"); | |
| 70 | + | // | |
| 71 | + | // 2222 is hardcoded here and is config on the host (`SSH_PORT` in | |
| 72 | + | // /opt/mnw-cli/.env, read 2026-08-23). Not read from that file on purpose: it | |
| 73 | + | // is owned by the service user and the deploy user would need sudo to see it, | |
| 74 | + | // which is a wider grant than an assertion is worth. If the port ever moves, | |
| 75 | + | // this line moves with it -- and it failing loudly is the correct outcome of | |
| 76 | + | // forgetting, since a release that cannot find the service it just restarted | |
| 77 | + | // should not read green. | |
| 78 | + | sh_ok(deploy_host(), "banner=$(timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/2222; head -c 4 <&3' 2>/dev/null); test \"$banner\" = 'SSH-' || { echo \"mnw-cli did not answer with an SSH banner on 2222: got '$banner'\"; exit 1; }"); | |
| 79 | + | ||
| 80 | + | log("mnw-cli " + v + " (" + target() + ") built on " + h + " and live on " + deploy_host()); |