How a site answers missing paths now belongs to its repository like its edge rules: bunny.toml declares [site] not_found as "default", "spa" or a page from the build such as "/404.html". The action reads and sends the table on start and finalize; the gateway validates it before the upload and applies it to the Storage Zone at finalization, recording and purging only when it changed. A deployment without [site] resets the default; older action versions leave the setting alone. The console's Settings tab only shows the applied value, and the admin API no longer sets it. The maintainer guide and llms.txt describe the setting under Configuration.
7.6 KiB
Static Site Deploy
A small, dependency-free action for Forgejo Actions and GitHub Actions that synchronizes a completed static website build to a dedicated Bunny Storage Zone.
The runner needs Bash and Python 3.11 or newer. The action itself installs no packages and executes no code from the Gandalf repository.
The deployer requires index.html, rejects symbolic links, compares files with
a checksum manifest, uploads changed assets before HTML, deletes stale files
only after successful uploads, and publishes the new manifest last.
Forgejo Actions
Store the website-specific Storage Zone password as the repository secret
BUNNY_STORAGE_PASSWORD, then pin the action to an immutable commit from the
public action repository:
---
name: Deploy website
on:
push:
branches:
- main
jobs:
deploy:
runs-on: docker
steps:
- uses: https://data.forgejo.org/actions/checkout@v6
- run: npm ci
- run: npm run build
- name: Deploy to Bunny
uses: https://forge.tionis.dev/actions/static-site-deploy@<commit-sha>
with:
directory: dist
storage-zone: example-org
purge-endpoint: https://gandalf.tionis.dev
env:
BUNNY_STORAGE_PASSWORD: ${{ secrets.BUNNY_STORAGE_PASSWORD }}
Only run deployments for trusted branches. A contributor able to change a workflow that receives production secrets can attempt to expose those secrets.
purge-endpoint is the backward-compatible name of the optional Gandalf
deployment gateway input. Before uploading, the action acquires an expiring
per-zone lease. After publishing the manifest, Gandalf verifies its SHA-256
digest, records the revision and deployment outcome, and purges the mapped Pull
Zones only when the manifest changed from the previous successful deployment.
Interrupted uploads are aborted best-effort; abandoned leases expire after 30
minutes. The dedicated Storage Zone password authorizes this workflow without
exposing an account-wide API key to the project.
Deploying with the repository's identity
With the Gandalf gateway, a site linked to its repository needs no secret at
all. Leave BUNNY_STORAGE_PASSWORD unset and let the job request an OIDC
identity token: the action presents it to the gateway, which checks the
repository and branch and hands out the Storage Zone password for this one
deployment. The action masks it in the log, and the gateway replaces it once the
deployment is done (unless the site also allows password deployments).
Forgejo:
jobs:
deploy:
runs-on: docker
enable-openid-connect: true
steps:
- uses: https://data.forgejo.org/actions/checkout@v6
- run: npm ci && npm run build
- uses: https://forge.tionis.dev/actions/static-site-deploy@<commit-sha>
with:
directory: dist
storage-zone: example-org
purge-endpoint: https://gandalf.tionis.dev
GitHub, using a GitHub mirror of this repository:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: <owner>/static-site-deploy@<commit-sha>
with:
directory: dist
storage-zone: example-org
purge-endpoint: https://gandalf.tionis.dev
A dry run in this mode only checks the build locally; comparing with the deployed files needs the password.
Edge rules
Bunny Storage has no _headers or _redirects file and picks a file's
Content-Type from its extension. With the gateway, a site declares Bunny edge
rules and site settings in a TOML file kept in its repository, outside the build
directory:
with:
directory: dist
storage-zone: example-org
purge-endpoint: https://gandalf.tionis.dev
edge-rules: bunny.toml
[[edge_rules]]
name = "markdown"
url = "*.md"
response_headers = { "Content-Type" = "text/markdown; charset=utf-8" }
[[edge_rules]]
name = "old-blog"
url = ["*/blog/*", "*/news/*"]
redirect = { to = "https://example.org{{path}}", status = 301 }
[[edge_rules]]
name = "retired"
url = "*/alt/*"
status_code = 410
[[edge_rules]]
name = "hashed-assets"
url = "*/_astro/*"
cache_seconds = 31536000
browser_cache_seconds = 31536000
Each rule has a unique name (lowercase letters, digits, -), at least one
trigger and at least one action. Rules run in file order; there are at most 25.
Triggers take one pattern or a list of up to 10, any of which may match:
| Key | Matches |
|---|---|
url |
The full request URL, e.g. *.md or */docs/* (* also spans /). |
method |
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. |
status |
The response status code, e.g. [404]. |
country |
Two-letter country codes such as "DE". |
request_header |
{ name = "X-Debug", values = ["1"] }. |
With several triggers, match decides: "all" (default), "any" or "none".
Actions:
| Key | Effect |
|---|---|
redirect |
{ to = "https://…", status = 301 }; status is 301, 302 (default), 307 or 308. to may use Bunny variables such as {{path}} (path with query string). |
status_code |
Replace the response status, 200-599. |
block |
true: refuse the request. |
response_headers |
Header names to values. Framing and hop-by-hop headers such as Content-Length are rejected. |
cache_seconds |
Edge cache time in seconds. |
browser_cache_seconds |
Browser cache time in seconds. |
ignore_query_string |
true: cache regardless of the query string. |
force_download |
true: serve as a download. |
redirect, status_code and block exclude each other. Bunny stops at the
first matching redirect or block; headers from all matching rules apply.
Actions that could reach beyond the site, such as changing the origin or
switching off security features, are not available.
The gateway checks the rules when the deployment starts, so a mistake fails
before anything is uploaded, and its error names the rule. A dry run only checks
the TOML structure. On finalization the gateway applies the rules to the site's
Pull Zones, removes rules dropped from the file, and purges the cache when a
rule changed. A deployment through the gateway without edge-rules removes the
rules of earlier deployments; rules set up by other means are never touched.
Site settings
The same file may hold a [site] table:
[site]
not_found = "/404.html" # or "spa", or "default" (also when left out)
not_found decides how a missing path is answered: Bunny's 404 page, a page
from the build with status 404, or the build's /index.html with status 200 for
single-page apps. Each deployment applies it to the Storage Zone and purges the
cache when it changed.
Command line
The script can also be used directly with Python 3.11 or newer:
export BUNNY_STORAGE_ZONE=example-org
export BUNNY_STORAGE_PASSWORD='zone-specific password'
python3 static_site.py bunny dist --dry-run
python3 static_site.py bunny dist
Changed files are transferred with 16 workers by default while preserving the
asset-before-HTML and manifest-last publication order. Set
BUNNY_TRANSFER_WORKERS or pass --workers to tune the limit.
Set BUNNY_STORAGE_ENDPOINT when the Storage Zone is outside Frankfurt.
Pass edge rules with --edge-rules bunny.toml (or STATIC_SITE_EDGE_RULES);
--dry-run checks only the file's TOML structure.
Development
python3 -m unittest discover -s tests
The canonical source lives in the private Gandalf infrastructure repository and is published to this repository as a Git subtree.