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
5 changes: 5 additions & 0 deletions .changeset/deploy-site-initial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"deploy-site": major
---

Add the `deploy-site` action: deploy a static site to bunny.net via the `@bunny.net/cli` `sites deploy` command, with per-deploy preview URLs, a sticky pull request preview comment, and preview cleanup on PR close (`sites deployments delete` of the deploy recorded in the comment; opt out with `cleanup: false`).
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
</a>
</div>

Github actions
====
# Github Actions

This is the repository where we put actions to use with
[Bunny](https://bunny.net)!
Expand All @@ -19,16 +18,16 @@ Each action can be used in your workflow this way:

```yaml
steps:
- uses: BunnyWay/actions/<action>@<actions@ref>
- uses: BunnyWay/actions/<action>@<actions@ref>
```

Each action will have its own documentation, you can check the associated documentation
in each folder.
Each action will have its own documentation, you can check the associated documentation in each folder.

It contains:

- [`BunnyWay/actions/deploy-script`](./deploy-script)
- [`BunnyWay/actions/container-update-image`](./container-update-image)
- [`BunnyWay/actions/deploy-site`](./deploy-site)

## Development

Expand All @@ -41,6 +40,6 @@ We handle versionning with [changeset](https://github.com/changesets/changesets)
to indicate the kind of changes you are doing so we can have the auto-release
process ongoing by doing:

```
```bash
pnpm changeset
```
3 changes: 3 additions & 0 deletions deploy-site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
coverage
.lib-action
1 change: 1 addition & 0 deletions deploy-site/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# deploy-site
136 changes: 136 additions & 0 deletions deploy-site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Deploy a static site to bunny.net

This GitHub Action deploys a built static site to [bunny.net](https://bunny.net)
with one `uses:` step. It wraps the [`@bunny.net/cli`](https://www.npmjs.com/package/@bunny.net/cli)
`sites deploy` command (the CLI is the single deploy path) and owns the sticky
pull request preview comment.

- Deploys a **preview** by default; publishes to **production** when asked.
- On `pull_request` events it upserts one sticky comment with the preview URL,
updated on every commit.
- Runs on `ubuntu-latest` and `macos-latest`. (Windows is not verified; the CLI
ships per-platform binaries and Windows availability has not been confirmed.)

## Usage Example

```yaml
name: Deploy site
on:
push:
branches: [main]
pull_request:
# `closed` lets the action delete the PR's preview deploy on merge/close.
types: [opened, synchronize, reopened, closed]

concurrency:
group: bunny-sites-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run build
- uses: BunnyWay/actions/deploy-site@deploy-site_1.0.0
with:
site: my-site
directory: dist
production: ${{ github.event_name == 'push' }}
api_key: ${{ secrets.BUNNY_API_KEY }}
```

> **Fork PRs:** pull requests from forks do not have access to repository
> secrets, so the deploy (and its preview comment) cannot run for them. The
> `if:` condition above skips fork PRs; pushes and same-repo PRs still deploy.

You can scaffold this workflow with `bunny sites ci init`. See the CLI repo's
framework examples (Next.js, Astro, Vite, SvelteKit) for per-framework build
commands and output directories.

## Inputs

| Input | Required | Default | Description |
| -------------- | -------- | --------------------- | -------------------------------------------------------------------------------- |
| `site` | yes | | Site name or storage zone ID. |
| `directory` | yes | | Built output directory to deploy (e.g. `dist`). |
| `api_key` | yes | | bunny.net API key (store it as a repository secret). |
| `production` | no | `"false"` | Publish this deploy as the live site (`"true"`/`"false"`, default preview only). |
| `comment` | no | `"true"` | Upsert a sticky PR comment with the preview URL on `pull_request` events. |
| `cleanup` | no | `"true"` | Delete the PR's preview deploy on `pull_request` `closed` events (see below). |
| `github_token` | no | `${{ github.token }}` | Token for the PR comment (needs `pull-requests: write`). |
| `cli_version` | no | `"0.13"` | `@bunny.net/cli` version range to run (pin bumped per action release). |
| `force` | no | `"false"` | Redeploy even when content is unchanged. |

The action always passes `site` explicitly (as `--site`) so CI never depends on
a `.bunny/site.json` manifest or `bunny.jsonc` being checked in, and it expects
`directory` to already be built: run your build as a previous workflow step
(the CLI's `deploy --build` convenience is for local use).

## Outputs

| Output | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| `deploy-id` | The deploy ID (git short sha on clean checkouts, else a content hash). |
| `preview-url` | Immutable preview URL for this deploy (empty when the preview zone is not ready yet). |
| `production-url` | The site's live URL (custom domain, else its b-cdn.net host; empty when the site has neither). |
| `promoted` | `"true"` when this deploy is the live production deploy — use this rather than assuming `production` decided it. |
| `unchanged` | `"true"` when the content was already deployed and nothing was uploaded. |
| `deleted` | `"true"` when a closed PR's preview deploy was deleted by cleanup. |

## How deploys behave

- **Every deploy gets its own preview URL** (`https://sites-dpl-<id>-<suffix>.b-cdn.net`):
root-served on its own host with HTTPS out of the box, so client-side routers
and root-absolute assets behave exactly as in production. Previews never need
a custom domain, and preview responses carry `X-Robots-Tag: noindex`.
- **Publishing is explicit**: previews are the default, and `production: true`
publishes the deploy as the live site. Promoting is instant — it flips a
router variable and purges the cache; no files move.
- **Deploys are immutable and content-addressed**: the deploy ID is the git
short sha when the checkout is clean, otherwise an 8-char content hash.
Re-deploying identical content is a no-op (the `unchanged` output is
`"true"`); set `force: true` to redeploy anyway. Dotfiles and `node_modules`
are never uploaded.
- **Rollbacks** don't need this action: `bunny sites deployments publish
--previous --force` flips production back instantly.

## Preview cleanup on PR close

When the workflow subscribes to the `closed` pull request type (as in the
example above), a merged or closed PR triggers a cleanup run instead of a
deploy: the action reads the deploy id from its own sticky comment, runs
`bunny sites deployments delete`, and rewrites the comment to say the preview
was deleted. Details worth knowing:

- The sticky comment is the source of truth — the deploy id can't be
recomputed at close time (PR runs deploy the merge-ref checkout, whose sha
isn't in the closed event). No comment (or `comment: false` on deploys)
means nothing to clean; `sites deployments prune` remains the catch-all.
- Only the **last** preview is deleted. Earlier per-commit previews on the
same PR stay until a `prune`.
- The CLI refuses to delete the live production deploy or the rollback
target, so a fast-forward merge (where the preview's id just went live)
degrades to a warning, never an outage.
- Cleanup never fails the job — failures are warnings, since a red run on a
closed PR blocks nothing and `prune` catches leaked previews later.
- Requires `@bunny.net/cli` >= 0.13.1 (the release that ships
`sites deployments delete`); the default `cli_version` pin resolves it.
With an older pinned CLI, cleanup warns and leaves the preview in place.

## Setting up the API key

Store your bunny.net API key as a repository secret named `BUNNY_API_KEY`:

```bash
gh secret set BUNNY_API_KEY
```

Or via the GitHub UI: **Settings → Secrets and variables → Actions → New
repository secret**.
60 changes: 60 additions & 0 deletions deploy-site/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Deploy Site to Bunny
author: Bunny Devs
description: Deploy a static site to bunny.net with preview URLs per deploy.

inputs:
site:
description: Site name or storage zone ID.
required: true
directory:
description: Built output directory to deploy (e.g. dist).
required: true
api_key:
description: bunny.net API key (store it as a repository secret).
required: true
production:
description: Publish this deploy as the live site ("true"/"false", default preview only).
required: false
default: "false"
comment:
description: Upsert a sticky PR comment with the preview URL on pull_request events.
required: false
default: "true"
cleanup:
description: On pull_request "closed" events, delete the preview deploy recorded in the sticky comment (subscribe the workflow to the "closed" type; requires @bunny.net/cli >= 0.13.1).
required: false
default: "true"
github_token:
description: Token for the PR comment (needs pull-requests write).
required: false
default: ${{ github.token }}
cli_version:
description: "@bunny.net/cli version range to run (pin bumped per action release)."
required: false
default: "0.13"
force:
description: Redeploy even when content is unchanged.
required: false
default: "false"

outputs:
deploy-id:
description: The deploy ID (git short sha on clean checkouts, else a content hash).
preview-url:
description: Immutable preview URL for this deploy (empty when the preview zone is not ready yet).
production-url:
description: The site's live URL (custom domain, else its b-cdn.net host; empty when the site has neither).
promoted:
description: '"true" when this deploy is the live production deploy (use this rather than assuming the production input decided it).'
unchanged:
description: '"true" when the content was already deployed and nothing was uploaded.'
deleted:
description: '"true" when a closed PR''s preview deploy was deleted by cleanup.'

runs:
using: "node24"
main: ".lib-action/index.js"

branding:
color: "orange"
icon: "upload-cloud"
10 changes: 10 additions & 0 deletions deploy-site/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";

export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
9 changes: 9 additions & 0 deletions deploy-site/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ["js", "ts"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
};
44 changes: 44 additions & 0 deletions deploy-site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "deploy-site",
"version": "0.0.0",
"main": "lib/main.js",
"private": true,
"scripts": {
"lint": "eslint src",
"test": "jest --silent --coverage",
"build": "ncc build src/main.ts -o .lib-action/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/BunnyWay/actions.git"
},
"keywords": [
"actions",
"github",
"bunny",
"sites",
"deploy"
],
"author": "Bunny Devs",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/jest": "^29.5.14",
"@types/node": "^22.19.15",
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"@vercel/ncc": "^0.38.4",
"eslint": "^9.39.4",
"globals": "^15.15.0",
"jest": "^29.7.0",
"prettier": "^3.8.1",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.2"
}
}
Loading
Loading