Deploying
blogwright deploy takes your repository from working tree to live site: it zips the source, builds it inside a Lambda MicroVM, syncs only the changed files to S3, and invalidates only the changed CloudFront paths. This page walks the whole lifecycle so you know exactly what each step does - and what it deliberately skips.
blogwright deploy # deploy to production (the default environment)blogwright deploy staging # or any other environmentbw deploy # `bw` is an alias for `blogwright`The environment defaults to production; see Environments for running several stacks side by side.
The lifecycle at a glance
Section titled “The lifecycle at a glance”- Find the repo root, resolve the current revision hash, and zip the deployable files. When
headersis configured, first bring the response headers policy’s headers in step with the config (the policy only, never the distribution). If the policy still needsbootstrap- it is not created or attached yet, orheaderswas removed - the deploy warns instead. - Upload the zip to
build/<hash>.zipin the environment’s S3 bucket. - Reconcile the builder MicroVM image if the build-agent bundle changed (no-op otherwise).
- Launch a builder MicroVM that pulls the zip, runs
pnpm install && pnpm build, and syncs the output tosite/, comparing each file’s MD5 to the live object’s ETag and writing each upload’sCache-Controlfrom thecacheControlrules. - Record the build’s artifact manifest - every published path, byte count and SHA-256 - at
build/artifacts/<hash>/<buildId>.json, and keep the first one recorded for the revision, write-once, atbuild/artifacts/<hash>.json. - Invalidate only the CloudFront paths that actually changed.
- On a successful production deploy, sync standard.site records to your PDS (when configured).
- Print a deploy summary card.
Packaging the source
Section titled “Packaging the source”The CLI finds the repo root by walking up from wherever you invoke it to the nearest .git or .jj, so blogwright deploy works from any subdirectory.
The revision hash names the build. With jj, it is the working copy’s commit id (jj auto-commits the working copy); without jj, it falls back to git rev-parse --short HEAD. The hash becomes the zip’s name (build/<hash>.zip), the key for build logs and manifests, and the argument you later pass to blogwright logs or blogwright rollback.
Which files ship
Section titled “Which files ship”The file listing honors .gitignore: tracked files plus untracked files that are not ignored (git ls-files --cached --others --exclude-standard). Two config knobs adjust it:
sourceIgnore- extra prefixes to drop. Defaults to.jj/,.git/,node_modules/,dist/,.astro/. An entry matches an exact path or a directory boundary:"dist"dropsdistand everything under it, but neverdist-notes.md.sourceInclude- gitignored paths to zip anyway, for artifacts a pre-deploy step builds outside the MicroVM (a wasm bundle, generated data). Run the producing step before deploying - a missing or emptysourceIncludeentry fails the deploy with a pointer to it, because shipping without the artifacts would deploy a broken site.
{ "region": "us-east-1", "siteName": "example", "sourceIgnore": ["fixtures/"], "sourceInclude": ["public/wasm"],}Tracked files that are deleted on disk are skipped rather than failing the zip. The zip itself is deterministic (fixed timestamps), and one extra file is injected in memory: .commit-hash, carrying the revision hash. The build runs from this zip with no .git present, so your site’s build config can read the deployed revision from that file instead.
See Configuration for the full key reference and Monorepos for sourceInclude in practice.
The builder image check
Section titled “The builder image check”Before launching the build, the deploy reconciles the builder MicroVM image: if the build-agent bundle shipped with your installed CLI version differs from the one baked into the image (compared by hash), the image is updated first. This means build-agent fixes from a blogwright upgrade land on the very next deploy, and when nothing changed the check is a no-op.
Right after an image update, the MicroVM control plane can briefly answer 502 for launches. The CLI retries gateway errors (502/503/504) with a bounded backoff of about two minutes; the launch is idempotent, so retrying is safe.
Building in the MicroVM
Section titled “Building in the MicroVM”The CLI launches a builder MicroVM from the snapshotted image, then writes a pending-job file to S3 describing the build: the source key, the target site prefix, the build directories (paths.app / paths.dist), object tags, the cacheControl rules, and the SEO policy. The agent inside the VM (booting in roughly 30 seconds) polls for this job, then:
- Downloads and extracts
build/<hash>.zipinto a work directory. - Runs
pnpm install --frozen-lockfile --prod=falseandpnpm run buildinpaths.app(the repo root by default). Dev dependencies are installed deliberately - static-site build tooling (astro, vite, tailwind) lives there. - Writes
robots.txtandsitemap.xmlinto the output directory per the environment’s SEO policy, so they publish and invalidate like any other page. - Syncs the output directory (
paths.dist, defaultdist/) to the site prefix in S3.
Build output streams live to your terminal as it lands in CloudWatch. The CLI decides the outcome from hash-scoped markers in the log stream, so output from an unrelated build can never be mistaken for yours. The build is bounded by microvm.maxDurationSeconds (default 1800); the VM is terminated when the build finishes and self-terminates at that limit regardless.
Every build - success or failure - records a manifest at build/manifests/<hash>.json with the status, timestamps, duration, and MicroVM id. blogwright history reads these; see Operations.
A successful static build also records an artifact manifest: every file it published, with its byte count and lowercase SHA-256, one aggregate release hash over all of them, and a release identity string (<version>+<hash>.<releaseHash prefix>, the version taken from your app’s package.json). Each build records its own at build/artifacts/<hash>/<buildId>.json; the first one a deploy records for a revision is also kept, never to be replaced, as its original at build/artifacts/<hash>.json. Deploying the same hash again leaves the original as it was, and warns if the new build is not byte-identical to it. The deploy prints the release and the manifest’s location so CI can archive it. A stack bootstrapped before artifact manifests existed cannot write it yet - the deploy warns until blogwright bootstrap <env> grants the builder build/artifacts/*/*.
Syncing only what changed
Section titled “Syncing only what changed”The agent does not blindly upload the build output. For each built file it computes the MD5 and compares it to the live object’s S3 ETag under site/; only files whose content actually changed are uploaded. Each upload carries the Cache-Control of the first cacheControl rule matching the file’s URL path - see Caching and Cache-Control. New files upload, unchanged files are skipped entirely, and stale objects (files no longer in the build) are deleted - but only after the new files are published, so the live site never has an empty or partial window.
The set of changed URL paths (each changed key mapped to the path a viewer would request) is written to build/changed/<hash>.json, which drives the next step.
Cache invalidation
Section titled “Cache invalidation”The CLI reads the changed-paths manifest and invalidates accordingly:
- Nothing changed - no invalidation at all. An identical redeploy uploads nothing and invalidates nothing.
- Up to
invalidationMaxPathschanged (default 1000) - only those URL paths are invalidated, so unchanged pages stay cached at the edge. Directory index documents invalidate both forms a viewer might request:site/posts/index.htmlbecomes/posts/index.htmland/posts/. - Over the cap, or the manifest is missing - a single
/*invalidation.
Tune the threshold with the invalidationMaxPaths config key; see Configuration.
How URLs map to files
Section titled “How URLs map to files”A CloudFront function resolves every URL without a file extension to a directory index: /about and /about/ both fetch about/index.html. It never tries about.html.
-
Astro already writes
about/index.htmlby default. -
SvelteKit’s
adapter-staticwritesabout.htmlunless pages settrailingSlash. Without this, every prerendered page other than/answers404:src/routes/+layout.js export const prerender = true;export const trailingSlash = 'always'; // writes /about as about/index.html
A path with no file behind it gets your /404.html with status 404 - so make sure your build writes one. With SvelteKit, give adapter-static fallback: '404.html'. Single-page apps answer these paths with index.html instead; see Single-page apps. Complete projects: SvelteKit SSG, Astro static.
robots.txt and sitemap.xml
Section titled “robots.txt and sitemap.xml”The builder writes robots.txt and sitemap.xml into your output before uploading, so they publish and invalidate like any other page. The seo config decides what goes in them, with defaults that depend on the environment:
| Setting | Default | What it does |
|---|---|---|
seo.robots |
"auto" |
auto makes production indexable and blocks crawlers everywhere else. index or noindex force either. off keeps whatever robots.txt your build wrote. seo.robotsContent replaces the body verbatim. |
seo.sitemap |
"auto" |
auto writes sitemap.xml in production only. on or off force it. It needs a site origin: your custom domain, or the CloudFront domain. |
The sitemap lists the HTML pages in your build output: directory index documents map to their clean URL, and 404.html pages are left out. A prerendered site gets one entry per page; a single-page app that writes one index.html gets one entry.
Metadata fixes: --refresh
Section titled “Metadata fixes: --refresh”S3 writes object metadata - the content type, object tags and Cache-Control - only on the PUT. Because the sync skips unchanged content, a metadata fix (an upgrade that corrects a content type, or newly added tags) never reaches an object whose bytes did not change. Pass --refresh once after such an upgrade to re-upload every file:
blogwright deploy --refresh--refresh is also accepted by rollback, preview deploy and stage; on promote it copies every staged file.
You do not need it for Cache-Control: each build records the cacheControl rules the builder confirms it applied, and a build whose rules differ from the target’s record, or whose last rule change did not finish, refreshes on its own and says so (an implicit --refresh). That includes the first deploy after upgrading to a version with cacheControl, which retrofits the default headers once. See Caching and Cache-Control.
The deploy summary
Section titled “The deploy summary”On a TTY, a successful deploy ends with a summary card: the revision, environment, source size (file count and KiB), build duration, the release identity and hash from the artifact manifest (static environments), what was invalidated (“nothing changed - skipped”, “12 changed paths”, or “everything (/*)”), and the site URL. Piped output and CI get the same information as stable line-oriented text.
Build logs
Section titled “Build logs”Everything the build printed - pnpm, the site build, the sync - lands in a CloudWatch log group (retention configurable via retention.microvmDays, default 365 days). Retrieve a build’s logs by its hash:
blogwright logs <hash>The command uses the hash’s newest build record - its deployment manifest, or its staged build’s record when that is newer - to window the log query to that build’s time span (plus a minute either side). If the manifest is unreadable it falls back to the unfiltered window. See Troubleshooting for reading a failed build’s output.
standard.site sync after deploy
Section titled “standard.site sync after deploy”When the config has a pds section and publishing has been initialised, every successful production deploy also re-reconciles the site’s standard.site records on your PDS. The sync is non-fatal by design: if it fails (an expired session, a network blip), the deploy still succeeds with a warning, and the next deploy heals the records. Non-production environments never sync.
A promote or rollback runs the same post-deploy steps only when the working copy is the revision it publishes: the PDS mirrors the working tree’s content, which matches the live site only then. With any other checkout the command warns and skips them; check out the published revision and run blogwright pds sync to realign the records. See Publishing to standard.site.
Release gating: stage, verify, promote
Section titled “Release gating: stage, verify, promote”deploy builds and publishes in one step. When a release has to be checked before it goes live - and what goes live has to be exactly what was checked - split it in two:
blogwright stage production # build once; publish to releases/<hash>/, not to site/blogwright verify <hash> production --staged # check the staged bytes against the staged release recordblogwright promote <hash> production # copy those bytes into site/ - no rebuildstage runs the same build as deploy - the same source zip, builder MicroVM, robots.txt and sitemap.xml for the target environment - but the builder publishes the output to releases/<hash>/ instead of site/, and records its own artifact manifest. Nothing serves releases/: the distribution’s origin is site/, and the bucket is private, so a staged release is not reachable from the domain or the CloudFront hostname. Nothing is invalidated. stage then reads every staged object back, checks it against that manifest, and only then records the manifest as the staged release (build/releases/<hash>.json) - the record promote checks against. It prints the release identity, its release hash and the promote command to run. stage --refresh re-uploads every file into the staged prefix, for a metadata fix on a hash that was already staged.
A staged release is bound to the build that staged it, not to the revision’s original manifest. If the revision has no original yet, the staged manifest becomes it, so verify <hash> after the promotion checks the same record. If the revision already has an original from another build with other bytes - it was deployed before, or staged before and the build is not reproducible - stage and promote say so: the promotion is still checked against the staged release, but verify <hash> checks the original and reports the differences. See Staging and promotion.
promote <hash> points production at that already-built output:
- It reads the staged release record and re-verifies the staged objects against it - every object read and its SHA-256 compared - and refuses, copying nothing, if any differs or the hash was never staged.
- It converges the response headers policy from the
headersconfig, asdeploydoes, so a header change reaches production with the release. - It copies each file whose live ETag differs from the staged one into
site/with a server-side S3 copy - or every file, in the cases below. A copy moves the staged object’s bytes, content type,Cache-Controland object tags exactly as they are; nothing is rebuilt, re-encoded or re-uploaded from your machine. A file whose content is unchanged is not copied, so it keeps the content type and tags it already had insite/: after an upgrade that fixes a content type, or once tags can be copied, runpromote --refresh(andstage --refreshfirst, if the staged copy needs the fix too). - It deletes the objects in
site/that the release does not contain, only after every copy has succeeded. - It invalidates the changed paths under the same rules as a deploy: the copied and deleted paths (each directory index also as its directory URL), or
/*pastinvalidationMaxPaths. When it copied every file, every path counts as changed. Only when the live site already held the release and nothing had to be copied for its headers is nothing invalidated. - It verifies
site/against the staged release record: every live object is read and hashed. The command succeeds only when the live release hash equals the staged one - the promoted bytes are the staged bytes. - It records the release as live (
build/live-release.json) and prints the release it replaced beside the one it published.
The copies run eight at a time, in no particular order, so while they run a visitor whose request misses the cache can get a new page whose new assets are not copied yet, or an old page beside a new asset - the same window a deploy’s upload has. Because stale objects are deleted only after every copy, no page that is still live loses an asset it references. If promote stops partway, site/ holds part of each release: the live-release record says a promote of <hash> did not finish (naming the last release that did), and running promote again finishes the job.
promote copies every file, not only the changed ones, when you pass --refresh, or when the staged release’s Cache-Control rules are not known to match the ones site/ was written with (see below).
The post-deploy plugin steps (the standard.site sync, for one) run after a promote only when the working copy is the promoted revision, because they publish the working copy’s content; otherwise the command warns and skips them.
Cache-Control on promote. A staged release’s files carry the Cache-Control the cacheControl rules gave them when it was staged, recorded at build/cache-control/release-<hash>.json. A server-side copy keeps that header, so the headers are promoted along with the bytes, as part of what was reviewed. A file whose content is unchanged would keep the header site/ already had, so when the staged rules are not known to match the ones site/ was written with - they differ, site/’s record is unconfirmed, the staged record is missing or unconfirmed, or site/ has no record while the staged rules are not empty - promote copies every file instead, after marking site/’s record unconfirmed so an interrupted promotion is re-applied. Afterwards site/’s record (build/cache-control/site.json) holds the staged rules. If the configured rules changed after staging, promote warns and still publishes the staged headers - stage again to apply the current rules - and the next deploy re-uploads every file with the current rules, as it does after any rules change.
stage and promote hold the environment lock while they run, like deploy, rollback and delete, so none of them can interleave its writes with another.
What it does not cover
Section titled “What it does not cover”- Reviewing a staged release in a browser. A staged release is deliberately not served. Review rendered pages through PR previews or a separate environment;
stageandpromoteguarantee that what goes live is byte-identical to the staged build, not that a person looked at it. - Promoting between environments. A release is staged in the environment it will be promoted into, so its
robots.txtandsitemap.xmlare that environment’s. Staging instagingand promoting toproductionis not supported: the two would differ in exactly those files. - SSR environments and the preview stack. Both commands refuse there. An SSR deploy already pins the exact artifact version a revision serves, and
rollbackreproduces it without a rebuild. - Edge caches.
verifyandpromoteread S3, not CloudFront. Until an invalidation completes, an edge location may still serve a copy cached from before it.
Staged releases stay under releases/<hash>/ until you remove them, which is what lets an earlier hash be promoted again - and what lets rollback <hash> to a staged hash republish its verified bytes instead of rebuilding. If storage matters, add an S3 lifecycle rule expiring old releases/ objects, or delete a prefix with aws s3 rm --recursive s3://<bucket>/releases/<hash>/ (and its record, build/releases/<hash>.json).
stage needs builder roles that may write releases/*; on a stack bootstrapped before staging existed it refuses before building and asks you to run blogwright bootstrap <env> once. The same bootstrap grants the CI deploy role s3:GetObjectTagging on releases/* and s3:PutObjectTagging on site/*, which promote uses to copy object tags. Without them it promotes untagged and warns once; after bootstrapping, promote <hash> --refresh copies the tags onto every file.
SSR environments
Section titled “SSR environments”When the config carries an ssr block, the same pipeline publishes a Lambda Web Function revision instead of a static site. The differences:
- Preflight. Before anything is zipped, the deploy sends a SigV4-signed
GETto the endpoint root. Any answer passes, including the placeholder’s503. A caller that is not authorized to invoke the endpoint (lambda:InvokeWebFunctionEndpoint) stops the deploy there, before anything is built or published. - Build. Still a MicroVM build. The agent packages
ssr.packageDir(defaultpaths.dist) into a deterministic.zipafter checking the entry file exists, rejecting any symbolic link in the package, and enforcing Lambda’s 512 MB unzipped bound. It uploads the artifact toweb/artifacts/<hash>.zip, publishes onlyssr.staticDirtosite/when configured (upload-only - stale keys are listed, not deleted), and writesweb/results/<hash>.json. - Revision. The CLI fingerprints the artifact digest, runtime, execution role, log group and revision-scoped settings. When that fingerprint already serves, no revision is created: only the static changes are invalidated. The exception is a revision whose own publish never finished (an earlier deploy stopped after the cutover): the deploy finishes it instead, probing the endpoint and invalidating
/*. Otherwise it creates one revision from the artifact’s object version and every setting (a revision copies nothing from the last one), pollsGetWebFunctionRevisionuntilActive, and fails with the revision’s own errors if it settlesFailed- the previous revision keeps serving. - Readiness probe.
Activeonly means Lambda prepared the code, so the deploy then probes the endpoint root until it answers below500. Only a ready revision is followed by deleting stalesite/keys and invalidating/*, because a new revision may change any cached page. - Restore. A
LatestRevisionendpoint has already cut over by the time the probe runs. If the probe never passes, the deploy re-creates the previous revision from its captured configuration, waits for it to becomeActive, probes it, and fails naming both revisions. It does not invalidate and does not delete stale keys, so pages still cached keep serving. If the restore does not serve either, the failure says the endpoint is not serving and gives theblogwright rollback <hash>command for the newest hash with a recorded revision.
The summary reports the revision id, whether it was created or found unchanged, and the invalidation that ran. blogwright history adds a revision column in SSR mode: a hash shows a revision id only once a revision built from it was verified to serve (its manifest’s webFunction block), and a later failed deploy or rollback of the same hash keeps it, so the hash stays reproducible by rollback. A hash showing - has nothing to reproduce from.
Rollback in SSR mode
Section titled “Rollback in SSR mode”blogwright rollback <hash> reproduces the revision that hash was deployed with, from the artifact version and settings recorded in its manifest - not from the current config, whose entryPoint or env may not fit the older artifact. It skips creation when that fingerprint is already serving, and runs the same preflight, probe, restore and invalidation steps as a deploy.
It falls back to re-running the MicroVM build when the manifest has no webFunction block (a static-era or failed deploy), the recorded artifact version no longer exists, or ssr.staticDir is configured - the last because an artifact carries only the server, and the fingerprinted assets its pages reference were removed from site/ by the deploys that followed.
Switching modes
Section titled “Switching modes”To turn an existing static environment into an SSR one:
- Add the
ssrblock to the environment’s config, withssr.shimfor the entry shim, and the packaging steps to your build (Server-rendered sites). blogwright bootstrap <env> --switch-mode- creates the Web Function, its role, log groups, origin access control and cache policy, and moves the distribution’s default behavior to the endpoint, which answers503from a placeholder revision until step 3 finishes.blogwright deploy <env>- publishes the first real revision.
A new environment needs no flag: bootstrap then deploy. Moving back to static is the same sequence with the block removed.
blogwright bootstrap refuses to change the mode of a bootstrapped stack without --switch-mode, because the distribution’s default behavior moves before a deploy in the new mode exists: static → SSR answers 503 from the placeholder until the first SSR deploy completes, and SSR → static serves whatever site/ holds until the next static deploy. With the flag, bootstrap converges the distribution in place and repeats the window as a warning. Until that bootstrap has run, deploy and rollback refuse, because the configured mode is not the one the distribution serves. Enabling versioning is part of the way in; it is never suspended, including when ssr is removed from the config. Every object overwritten or deleted from then on - each redeployed site/ file, each state save - keeps its previous version, and Blogwright adds no lifecycle rule to expire them. If storage matters, add a noncurrent-version expiration rule yourself, scoped away from web/artifacts/: rollback reproduces revisions from the artifact versions their manifests record.
Removing ssr and re-bootstrapping does not delete the Web Function, its role, log groups, origin access control or cache policy: the graph is derived from the config, so once the block is gone those nodes are no longer part of it. An idle function costs nothing to run, but its log groups keep their retained logs, and destroy will not remove any of them either. To remove them, run blogwright destroy <env> --yes while the block is still present, or delete them by hand under the names in Architecture. See Server-rendered sites for the full contract.
Related pages
Section titled “Related pages”- Operations -
history,verify,rollback,status, and day-two tasks - Artifact manifest - the record
verifyandpromotecheck against - PR previews - the same build pipeline targeting
previews/<id>/site/ - CI deploys with GitHub OIDC - running
blogwright deployfrom a workflow - Caching and Cache-Control - the per-path headers, and browser versus edge lifetimes
- Architecture - the bucket layout and the builder image in depth
- CLI reference - every command and flag