Deploying
./blog.sh rebuild = build + deploy with --prune in one step; the publish/edit flows run it for you. The deploy script alone:
./scripts/deploy-web.sh # only new/changed files
./scripts/deploy-web.sh --dry-run # print what would happen, touch nothing
./scripts/deploy-web.sh --prune # also delete files the build no longer generates
./scripts/deploy-web.sh --force # ignore the manifest, re-upload everything
./scripts/deploy-web.sh --only=A,B # just the listed files
Things worth knowing:
- The safety guards. Four of them, all measuring this build against the last build a deploy accepted -- recorded in
.deploy_baseline.jsonbefore the first byte goes out, so no upload failure can move it:
| Guard | Trips at | What it does |
|---|---|---|
| File count dropped | >20%, at least 8 files | stops |
| Total bytes dropped | >50%, at least 25 MB | stops |
| File count grew | >20%, at least 25 files | stops |
| Total bytes grew | >50% | says so, continues |
A drop almost always means a broken build; the byte version catches what counts cannot -- the same pages, each nearly empty. Growth in bytes is only a notice, because adding a video is authoring, not a malfunction; a single file too big to host is caught separately and by name (below). If a swing is genuinely intended (bulk import, mass deletion), rerun with --force. An empty build is always refused.
The absolute floors keep the percentages usable on a small site, where two posts published at once would otherwise read as an explosion.
A change of the site's languages is not a swing. Adding one grows the build by a whole tree (/en/, every listing, tag and feed in it) and removing one shrinks it by as much, so the deploy says the languages changed, leaves those trees out of the comparison and holds the rest of the site to the limits above -- a broken build on the same day still stops. The files of a language the site no longer publishes are deleted from the target by that deploy, without --prune.
A drop also measures against the manifest when that is larger, since every entry in it is a file that really did upload. Growth never does: the manifest legitimately lags the build after a failed upload or on a fresh target, and reading that lag as growth is exactly what used to disable these guards. - One file-size limit, everywhere. A single file over 100 MB is refused -- when the post is saved (so you can still shrink it) and again before a deploy sends it. The same limit applies to every backend so the site stays portable: the strictest supported target (git pages) refuses anything larger. --force does not lift it, since the target would refuse the file on every run. Files between 50 MB and 100 MB are named but allowed. A file already on the target from before this limit existed is reported, not refused. - --prune is the only destructive flag. Without it, files the build stopped generating stay live on the target (the deploy log counts these "orphans"). With the git backend every deploy is a snapshot and prunes implicitly -- the log says "(snapshot deploy)". - The previous run's outcome is reported, not acted on. A deploy that failed or was interrupted says so at the top of the next one, and after three unfinished runs in a row it says that too -- however high the count, since stopping after N attempts would be its own dead end. - Manifests are disposable. .deploy_manifest*.json (one per backend) records what the target already has. Deleting one is always safe -- the next deploy re-uploads everything once and rebuilds it. The guards are unaffected, because their reference lives elsewhere. - ...with one exception worth knowing. rsync, rclone and git diff the target themselves, so a lost manifest costs one full re-upload and nothing else. For sftp, Surfer and a local directory the engine never reads the far end, so the manifest is also the only record of what stands on it: a file you delete at home after the manifest went missing stays on the target, and no flag will find it again. This is why a deploy says out loud that the manifest was written for another target instead of quietly starting over -- and why the target's identity ignores the switches that cannot move a connection (-v, -q, the order they are written in), so reformatting a line in env.sh is not a new target.
- Switching backends starts from a fresh manifest on purpose; the first deploy to a new target uploads the whole site. The baseline is shared across backends -- it describes the build, which is the same wherever it goes -- so switching targets no longer leaves the guards with nothing to compare against.
Checking the guards by hand
--dry-run needs no target and writes nothing, which makes it the way to prove the guards still behave before trusting a release. Copy a build to a scratch directory, point DEPLOY_TARGET_DIR at a throwaway path with DEPLOY_BACKEND=local, and work through the cases that are easy to get wrong:
| Set up | --dry-run must |
|---|---|
| Delete one post from a small site | pass -- the absolute floor covers it |
| Delete most of the build | stop, naming the accepted build it compared against |
| Publish two posts at once on a small site | pass |
| Duplicate the build | stop |
| Baseline intact, manifest truncated, build complete | pass -- this is recovery after a failed upload, and it is the case a naive fix breaks |
| Same, but the build is also broken | stop |
| Same file count, contents emptied past 25 MB | stop on bytes |
| Add one 60 MB file | pass, with a notice |
| Add one 120 MB file | stop, naming the file |
Empty public.nosync/ | stop |
Delete .deploy_baseline.json | pass, with no stand-down notice -- the growth guard borrows the file count from the manifest |
| Delete the manifest too | pass, saying the growth guard stands down once |
| Any of the above | leave .deploy_baseline.json untouched -- a dry run is read-only |
Two things make this easier to reason about: the failure state is anything that leaves last_run.outcome in .deploy_baseline.json set to something other than ok, and a run under --only must never change that file at all (that is how the sidebar cron stays out of the way).
