Tooling · for everyone, before a deploy

304. Checking a deployment

The question this chapter answers: what is wrong with an application that starts, serves and answers, but does not do what its configuration says it does?

What the host already refuses

The host is strict at boot on purpose. A declared action nothing answers, a bundle carrying a server module, a route claimed twice, a plan file it cannot read, a server.render that is neither rust nor islands: each of those stops the process with the reason, because a deployment that half works is worse than one that does not start.

That strictness has an edge. Everything it catches is a contradiction the host can see from inside a single boot. What it cannot see is a setting that is coherent, loads cleanly and still cannot do the job it was written for. A locale in the table with no catalog file loads: t falls back and the page renders in the wrong language. An import map naming a package nothing vendored loads: the browser asks for the file and gets a 404. A plan older than the routes it was lowered from loads perfectly, then answers yesterday's routes.

fsr doctor is that middle.

text
fsr doctor app

It reads the configuration and the plan the way the host would, runs every check and prints what it found with what to do about it. Nothing found is exit 0. Anything found is exit 1, so a deploy script can stop on it:

sh
fsr build app && fsr doctor app

fsr bundle runs the same checks itself and refuses on a finding, so a deploy that ends in a bundle needs no separate step:

sh
fsr build app && fsr bundle app

The command stays worth running on its own in CI, where there is a plan to check but no tree to write. fsr bundle --no-doctor skips the check for a caller that means to bundle anyway.

What it checks

Each check answers from something a build already computed, so none of it needs a server, a browser or a network.

CheckWhat it reportsWhy it matters
canonical[document] origin is unset while the deployment names hosts or prerendersWithout an origin the canonical and alternate links are relative, which an audit reports and a crawler resolves against whatever host it arrived on
ctx.hosta body reads ctx.host while [server] hosts is emptyThe list is the whole opt-in, so an empty one means the read answers null for ever rather than the host the request carried
ctx.configa body reads ctx.config.<key> while [public] does not declare the keyThe declaration is what types the read and what an overlay sets, so a key missing from it answers null on every deployment
localesa locale in [locales] supported with no catalog under locales/The application says it serves that language and every message falls back
stalethe plan is missing or older than routes/, src/, clients/ or schemas/The host reads the plan and never the sources, so an unbuilt change is invisible until the next build
vendorthe import map names a package with nothing under vendor/ to answer itThe browser asks for the file the map names, so a missing one is a page that does not mount
render[server] render is islands and the plan carries no islandEvery page is handed to the browser to render, for no reason
staticsa [[static]] root whose directory is not thereEvery path under that route answers 404, including the client bundle when it is served that way
shadowa [[static]] root whose route swallows an application routeA matched static prefix answers from the directory and returns, so the page underneath it never runs
bearera client carries a bearer token while [auth] is unsetAn [auth] provider is the only thing that writes a token into custody, so the call goes out with no Authorization header
cache.tagsa call drops a cache tag no cached method namesThe two sides are strings that have to agree; a typo either way leaves a write that invalidates nothing
linksa literal internal link matching no route, static root or mounted siteThe plan already holds the link and the routes, so a 404 nobody would find without clicking is findable without a crawler
treea file a deploy tree would carry that the project does not hold; a setting no tree can expressThe host reads each of these at boot, so a tree without one starts on the machine that built it and fails on the machine it was copied to
sitesa mounted site that pins no hash, ships a part the artifact does not carry, has no plan or one older than its own routes, plus artifacts under the root no mount namesA shell serves a site it never builds, so nothing about the artifact is checked until a request asks for it

A report names the check, the fact and the remedy:

text
canonical    `document.origin` is unset while `server.hosts` names 2 hosts, so every canonical and alternate link is relative
             set `[document] origin` to the address this deployment is reached at, `https://example.com`
doctor       1 of 14 checks found something

What the plan and the configuration say about each other

Four of the checks compare two artifacts that were written separately and have to agree.

shadow is the one with a boot error beside it. The host refuses to start when two plans claim one route and says nothing when a static root claims one, even though the outcome is worse: a matched static prefix answers and returns rather than falling through, so the page under it is unreachable for as long as both are declared.

bearer and cache.tags are both a name that only works if two places spell it the same. A bearer client reads a token out of custody and an [auth] provider is the only thing that puts one there. A cache tag is dropped by whatever names it in writes. Only the write side of a tag is asked about, because a cached tag nothing writes is how a read-only service says it expires by its own ttl. A typo in either direction leaves a written tag nothing caches.

links reads the literal href of every anchor the build lowered and asks whether this deployment answers it, against its routes, its static roots, the prefixes of the sites it mounts and the framework's own paths. A locale prefix is stripped first, the way the host strips one. Only a literal is asked about, since a computed href is not something the build knows the whole of. A site is exempt: its links reach into a shell it cannot see, so the shell it was built against need not be the one it runs in.

What a deploy tree would carry

The other checks read settings. This one reads the deploy tree that fsr bundle is about to write, before it exists, then reports a file the tree would name and the project cannot supply.

That is a class of failure the other checks cannot see, because the files it covers are not named in any setting. The host imports each service client's document at boot and will not start without it. It reads locales/ by name and an application whose catalogs went missing serves message keys. Neither is a path anybody writes down, so the only way to check them is to derive what the tree holds from what the host reads, which is what the bundle does anyway. Chapter 303 describes that derivation.

The check also refuses a setting no tree can express. A [[static]] route that climbs out of its own prefix has no place to land under serve/, so the layout stops rather than picking one.

The plan and the import map are left to stale and vendor, which already report them with remedies of their own.

What a shell owes its sites

A shell serves a mounted site and never builds it, so the artifact is the only thing that says what it should carry. Three of those findings are worth spelling out.

A mount that pins no hash accepts whatever sits at the path. The pin is what makes a deploy reproducible; fsr sites hash <site dir> prints the one to set. Only a name@version artifact is asked for one: a mount naming a path is a linked working tree that changes on every build, so a pin there would be stale by the next one.

A part the artifact says it ships and does not carry is hashed as absent rather than refused, so the site mounts and then answers 404 for its own assets. That happens when a site is packed without being rebuilt.

Artifacts under the sites root that no mount names are what fsr sites install leaves behind. They cost disk and they make it hard to tell which version is live; --keep <n> bounds them.

A pin the artifact no longer matches is reported with what to do about it. The content moved. If that was meant, fsr sites pin records it. If it was not, the directory is not the version the shell pinned. A mount pointing at nothing is reported too. Both are refusals to start rather than warnings; doctor says them before the deploy rather than instead of it.

What it will not do

It never fixes anything. Every finding here has a remedy that is a judgement: whether a locale should gain a catalog or leave the table, whether an island is missing or the render mode is wrong. A flag that picked one would be wrong half the time and silent about it.

It never softens a boot error. Anything the host refuses to start over still fails at boot. Doctor reports what has nowhere else to be reported; it does not downgrade failures.

It has no opinions. Every check covers a fact the application stated and then contradicted, never a matter of taste. "The locale table names fr and there is no fr catalog" is a fact. How long a title should be is not. There is no configuration file for turning checks off either, because there is nothing here worth turning off.

Where it belongs

In CI after the build, then in a deploy script before the bundle. It is fast and needs nothing running, so it is the last thing that reads the whole configuration before a server does.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.

Proudly Created by Excerion Sun LLC