main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
50cb77556d |
Deploy the backend, not just the frontend
`compose up -d` decides whether to replace a container by comparing its configuration, and the configuration is the string `runway:latest` whether or not the image behind that tag has moved. It never moves. So every deploy since this script was written swapped the frontend and left the backend exactly where it was, and the first one to matter was the one just now: the sidebar's new editor asks `/api/calendars` for `own_color`, and the backend answering it was built three days before that field existed. This is the third variation on one theme and the worst of them. The first failed loudly, the second failed quietly and exited 1; this one reports success, and what it leaves behind is precisely the state the single-image design exists to prevent -- a frontend calling an API the running backend does not have. `--force-recreate` is the fix. The check after it is the part worth keeping. Nothing above it could have caught this, because nothing failed: podman was asked to bring a service up, it was already up, and it said so. So the last thing the script does before recording a deploy is compare what is actually running against what it just pulled, and refuse to write the stamp if they disagree. A run that half-happens now leaves no record, which is what makes the next tick try again. |
||
|
|
e1e253319e |
Stop the deploy succeeding without deploying
The chown from the last commit unblocked the updater, and the updater then went on not deploying -- quietly, and now with exit 0. Three separate reasons, none of which the script could see from where it stood. It compared the image it had just pulled against the image it had before, which answers "is there anything new?" rather than "is what is running current". The failed run had already pulled, so the next run saw nothing new and stopped. It now records the image in `dist.deployed` at the end of a run that finished, and compares against that; a run that dies half way leaves no record and the next one does the whole thing again. It deployed into the wrong podman. The stack on this machine -- Caddy, Authelia and forty others -- is root podman, and the timer was a user unit, so `podman compose up` built a perfectly healthy backend in a rootless namespace that Caddy has no route to, and said so approvingly. The unit is now a system unit. That failure is worth naming because nothing local to the script can detect it: the container starts, the command succeeds, and the only symptom is somewhere else entirely. And Caddy could not see the frontend anyway. A bind mount holds an inode, so renaming `dist` out from under it leaves Caddy serving the directory that was there when it started -- which is also why rolling back by `mv`, as the README promised, would have done nothing at all. Caddy now mounts `web/`, the holder, so the rename happens inside what it can see. The database stays outside that directory, which is the reason for the extra level rather than mounting `data/runway` itself. One more, found by fixing the second: a container started from a `oneshot` unit inherits that unit's cgroup, so systemd tore down the backend the moment ExecStart returned. `restart: unless-stopped` put it back, which is precisely what made it hard to see -- the site was fine and the unit was red. `KillMode=process` leaves the container where podman put it. |
||
|
|
ff52fa1a1c |
Say why a deploy could not happen
The updater pulled the image, failed, and exited 1 with nothing in the journal explaining itself. The cause was ordinary -- `~/data/runway` is root-owned, left that way by v1, which deployed by `sudo rsync` over SSH and so never needed a user to write there. Replacing the frontend swaps one directory for another, which is a write to the directory holding them. The permission is the smaller half of this. A deploy that fails without saying why is worse than one that fails loudly for a stupid reason: the timer runs every ten minutes, so it would have gone on not deploying, quietly, at a distance from anyone reading the log. The check now runs before the pull and prints the exact `chown` to fix it, and the requirement is written down in the setup steps where it should have been in the first place. |
||
|
|
8df5860c75 |
Build the image on the branch that exists
The release workflow watched `master`. The repository's branch is `main`, so the job that builds and pushes the image would have sat there never firing, and the first sign of it would have been a deploy that never arrived. The check workflow runs on every push and so was never affected, which is exactly why this needed finding on purpose rather than by noticing. |
||
|
|
b0da3afafc |
Build and ship both halves from one commit
The audit's other "do it differently" (F76). v1's backend was an image built by CI; its frontend was `rsync -azX --delete` from a laptop, run by hand, with the API's absolute URL compiled into the WASM by an environment variable set in the same script. Two artefacts, one pipeline, and nothing keeping them in step. Here there is one image with the server binary at `/usr/local/bin` and the built frontend at `/srv/dist`, and the frontend asks for `/api/...` relative to wherever it is served -- so the artefact is the same in every environment and the two halves cannot be deployed apart. Caddy still serves the static files from a directory, because that is what the reverse proxy in front of everything already does; the updater lifts them out of the image rather than out of a build on somebody's machine. It stages them and moves the directory into place, since `index.html` names hashed files and a browser that fetches new HTML with old JavaScript gets a blank page. The image is 97 MB and holds no build tooling. Migrations are compiled into the binary and run at startup, and the database is created if it is missing, so there is no `sqlx-cli`, no entrypoint script, and nothing that can decide to carry on after a failed migration -- v1's start.sh ran migrations with `|| echo "Migration failed but continuing..."`. TLS is rustls with its roots compiled in, so there is no OpenSSL to keep patched. There is no dummy-source dance for dependency caching either; a BuildKit cache mount does what that trick was inventing, and the binary is copied out of the mount because a cache mount is not part of the layer. Deployment is a timer on the server rather than CI reaching into it. Nothing in the workflow holds a credential for the machine it deploys to, and a bad build cannot take the site down on its own; the cost is a few minutes between push and deploy, and `deploy/runway-update` for when that is too long. The rest of this is the CI that was promised at M1 and never written. "Rules that only live in a doc get forgotten, so these are lints and CI checks." The lints landed. The CI half did not exist, which meant for twenty-seven milestones the forgetting was still perfectly possible -- it was just mine rather than the repository's. `cargo fmt`, `clippy -D warnings` and the whole test suite now run on every push, alongside the three guardrails that were only ever configuration: `cargo machete` passes. `deny.toml`, written at M1, did not: three permissive licences its allow-list had not anticipated, and two `unmaintained` advisories arriving through Leptos's macros. Both are now allowed by name with a date and a reason rather than by widening a category. MPL-2.0 came out of the list, because nothing uses it and an allow-list should say what is actually there. That file had been quietly wrong since the day it was written, which is the argument for CI in one line. The bundle budget is 1.8 MB against today's 1.31 MB and v1's 2.5 MB, printed on every run. A bundle grows one convenient dependency at a time. The print rule is deliberately not in CI: it already exists as a test, it covers the CalDAV client too, and a shell grep cannot tell a call from a comment about a call -- the first draft of that step failed on the paragraph in `observability.rs` that explains the rule. Two things the image found that reading could not. `/db` is created in the image now. SQLite creates the database file if it is missing but not the directory holding it, so the container started only when something happened to be mounted there and otherwise died with "unable to open database file", which says nothing about what is wrong. And the graceful shutdown only listened for Ctrl-C. A container runtime stops a service with SIGTERM, waits ten seconds, and sends SIGKILL -- so `podman stop` took ten seconds and killed the process outright, and the handler written to stop a restart dropping a CalDAV write half-way through worked everywhere except deployment, which is the one place restarts happen. It listens for both now, and the container stops in two. |