Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/deploy-health-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@bunny.net/cli": patch
---

`bunny sites deploy` asks the site for a page before it calls the deploy a
success.

A published Edge Script that will not start makes the edge answer 400 with an
empty body, and the deploy said nothing: a green line, a URL, and a site that
served nothing. `withastro/astro.build` deployed exactly like that.

The check probes the production URL up to three times, each with its own query so
the CDN cache cannot hold the answer. A redirect or a 404 counts as a working
script; only 400 and 5xx are faults, and a site that cannot be reached at all is
not called one. When the script is above 7.5 MB the warning says so, because that
is where the trouble starts: measured in August 2026, the same code served every
request at 7.44 MB and none at 7.83 MB, well under the documented 10 MB.

`--output json` carries `serving`, and `status` when it is not.
16 changes: 16 additions & 0 deletions .changeset/deploy-polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@bunny.net/cli": patch
---

Four smaller things around `bunny sites deploy`.

- `--name <name>` is honoured when the deploy creates the site. Without it an
unattended run stopped with "No site specified and no linked site found."
- `--region <code>` chooses the storage region for a site the deploy creates.
Only `sites create` could name one before.
- The domain prompt after a first deploy refuses a value that is not a hostname,
and says so. It used to send it, and the API's answer is `An error has
occurred.`
- The upload counts bytes as well as files. `withastro/astro.build` sends 1.4 GB
in 8828 files, and ten minutes of `4210/8828 files` says nothing about how much
is left.
62 changes: 62 additions & 0 deletions .changeset/framework-deploys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
"@bunny.net/cli": minor
"@bunny.net/config": minor
---

`bunny sites deploy` now deploys a project that renders per request, as well as a
directory of files.

One command, and the build decides which it is. A build that writes
`.bunny/build.json` and asks for a server renders per request in an Edge Script,
with its client files in Bunny Storage. `bunny sites deploy` provisions the
storage zone, the script, and the pull zone on the first run, uploads the build,
sets every variable from what it already knows, applies the pull zone settings the
adapter asks for, and publishes. No password passes through the terminal.

The manifest is the whole contract: `BuildManifestSchema` in `@bunny.net/config`.
The CLI knows no framework, so a new adapter needs no new CLI.

Only a project that asks for a server hears about an adapter. Four things count
as asking: the Astro config names another vendor's adapter, the config sets
`output: "server"`, `@bunny.net/astro-adapter` is already a dependency, or a
route under `src/pages/` sets `prerender = false`. A project that prerenders
every page is a directory of files, and it deploys exactly as it did before: no
adapter, no config edit, and no mention of either.

The route scan is what puts the offer in the right place. Since Astro 5 a project
prerenders every page unless a page opts out, and `astro build` stops with its own
error when a page opts out and no adapter is installed. So the offer arrives
before that failure, not after it.

The adapter offer was measured against three real projects: `withastro/starlight`,
`withastro/astro.build` and `arthelokyo/astrowind`. What it learned:

- A monorepo root is not a project. At the root of a workspace the command looks
for the projects below it, and offers them. Starlight keeps `astro` in the root
`package.json` for `astro check`, and its site is `docs/`.
- The package manager comes from the nearest lockfile up the tree, not from the
directory. `starlight/docs` has no lockfile, so it looked like npm, and `npm
install` stopped on `workspace:*`.
- `pnpm add` at a workspace root gets `-w`, and Yarn's gets `-W`. Both refuse
without it.
- A project that already has another vendor's adapter gets it replaced, in one
edit that names both: `Replace @astrojs/cloudflare with
@bunny.net/astro-adapter in astro.config.mjs?`
- The adapter is added to the config, and `output` is not touched. Setting
`output: "server"` on a project that never mentioned it turns every prerendered
page into one that renders per request: on astro.build that took the script from
7.83 MB to 22.30 MB, past the 10 MB limit.
- The build and the 10 MB check both run before any resource is created.
astro.build used to leave a storage zone, a script and a pull zone behind on the
way to that error.

Each deploy keeps its client files at `deploys/{id}/` and its server bundle at
`_bunny/deploys/{id}/server.js`, and the CLI writes the deploy's folder name into
the top of the bundle at publish time. So a published release can only read the
files it was built with, and `bunny sites deployments publish --previous`
restores a page and its assets together. Deleting or pruning a deploy now deletes
its server bundle with it.

A build and a site have to be the same shape. A script's type is fixed when the
API creates it, so a static site cannot serve a server build, or the other way
round. The deploy says which mismatch it found, before it uploads anything.
38 changes: 38 additions & 0 deletions .changeset/router-static-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@bunny.net/cli": minor
---

Serve a static site's 404 page, redirects, and headers from the router.

Router v4 reads three file names out of the deploy it is serving: `404.html`,
`_redirects`, and `_headers`. Cloudflare Pages and Netlify read the same three,
so nothing in the router knows about a framework and every preset gets it.

- **`404.html`** answers a path the deploy does not hold, at status 404. Without
it the pull zone answers with bunny.net's error page, whatever the site built.
That shipped: a documentation site went up and every wrong URL showed
bunny.net's page.
- **`_redirects`** sends a real redirect. One rule per line, `/from /to [status]`,
`#` comments, a trailing `*` captured as `:splat`, and `!` to beat a file at the
same path. 301 is the default status; 302, 303, 307 and 308 are read too. A
rewrite (`200`) is not: it would have the router fetch another path of its own
site, which can be made to loop.
- **`_headers`** carries the headers Bunny Storage cannot hold. A `/path` line
opens a block, `Name: value` lines under it belong to it, and a later block
wins the same name.

A rule and a header match on a trailing-slash-normalised path, so `/about` and
`/about/` are one rule. The rules are read once per deploy and held in memory,
never written into the script: one script serves production and every preview, so
a promote stays an environment variable change.

The router now sets `Cache-Control` on every response, and a site's pull zone
stops overriding it (`CacheControlMaxAgeOverride: -1`). The zone default of 30
days replaced every answer the script gave, so an HTML page could be a month
stale in a browser that no purge reaches. A page now gets 60 seconds, anything
else 30 days as before, and `_headers` wins where it says anything.
`bunny sites upgrade-router` applies the router and the setting together, and
`bunny sites deploy` does it for a site whose router lags.

`bunny sites deploy` also asks the published site for a path it cannot hold, and
reports when the answer is not the deploy's own 404 page.
Loading
Loading