All checks were successful
Test / test (push) Successful in 9s
Without BUNNY_STORAGE_PASSWORD, the action now requests the running workflow's OIDC identity token (Forgejo with enable-openid-connect, GitHub with id-token: write) for the gateway's audience and starts the deployment with it. It takes the Storage Zone password from the gateway's answer, masks it in the log, plans and uploads with it, and finalizes or aborts with a fresh token. A dry run in this mode stays local. Password deployments work as before. action.yml finds its script under FORGEJO_ACTION_PATH or GITHUB_ACTION_PATH, so the action also runs on GitHub Actions.
208 lines
7.1 KiB
Markdown
208 lines
7.1 KiB
Markdown
# 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:
|
|
|
|
```yaml
|
|
---
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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 in a TOML file kept in its repository, outside the build directory:
|
|
|
|
```yaml
|
|
with:
|
|
directory: dist
|
|
storage-zone: example-org
|
|
purge-endpoint: https://gandalf.tionis.dev
|
|
edge-rules: bunny.toml
|
|
```
|
|
|
|
```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.
|
|
|
|
## Command line
|
|
|
|
The script can also be used directly with Python 3.11 or newer:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|