- Shell 74%
- JavaScript 26%
| action.yml | ||
| comment.mjs | ||
| preview.sh | ||
| README.md | ||
preview-action
Pull request previews on debianrb for any repository on this Forgejo: build the PR, run it as a container, get a link in a PR comment. No Vercel, no per-project setup beyond one workflow file.
https://<slot>.preview.rensebakker.nl -> container preview-<slot>:3000 on the `edge` network
Use
.forgejo/workflows/preview.yml in the repository:
name: Preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
concurrency:
group: preview-${{ github.event.number }}
cancel-in-progress: true
jobs:
preview:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: https://git.rensebakker.nl/brense/preview-action@main
The URL form of uses: is required (a bare owner/repo resolves against data.forgejo.org), and this repository
must stay public for other repositories to fetch it. closed in types is what tears the preview down.
A monorepo runs the action once per app with a matrix:
strategy:
matrix:
include:
- { name: site, path: apps/rensebakkernl, build: bun run build:site }
- { name: ui, path: packages/ui, build: bun run build:storybook }
steps:
- uses: actions/checkout@v4
- uses: https://git.rensebakker.nl/brense/preview-action@main
with: { name: '${{ matrix.name }}', path: '${{ matrix.path }}', build: '${{ matrix.build }}' }
What it does
On opened / synchronize / reopened:
- install —
bun install --frozen-lockfileif there is abun.lock,npm ciifpackage-lock.json, else nothing. (/ci-cache/bunis used when the runner provides it.) - build —
bun run build/npm run buildat the repository root if it has abuildscript, else inpath. - package, first match in
pathwins:Dockerfile.preview— your image, your rules.Dockerfile— same..output/server/index.mjs(a Nitronode-serverbuild) — wrapped innode:24-bookworm-slim.- a static site in
dist,build,storybook-staticorout— served bynginx:alpinewith SPA fallback.
- run as
preview-<slot>onedge:--memory 512m --cpus 1, no published ports,PORT=3000 HOST=0.0.0.0 PREVIEW_URL=https://<slot>...in the environment, plus anything from theenvinput. - wait for it to answer on 3000, then comment the URL (one comment per
name, edited on every push).
On closed: remove the container and image, release a custom slot, update the comment.
The container must listen on port 3000. nginx routes every slot to :3000 and cannot read per-container
settings. Node servers that honour PORT and the generated images already do; a Dockerfile.preview must.
Slots
| hostname | assigned | lifetime | |
|---|---|---|---|
| pool | s1..s8 |
lowest free; when full, the least recently updated pool preview is evicted | until the PR closes, is evicted, or goes 7 days without a push |
| custom | any [a-z0-9]{1,24} that is not s<n> |
the slot input, or a slot:<name> label on the PR |
until the PR closes (30 days idle at most); never evicted |
Why a pool: Let's Encrypt allows ~50 new certificates per domain per week and this Forgejo sees 40–65 PRs a week, so a hostname per PR is not possible. A PR keeps its slot across pushes; the comment is the source of truth for the URL.
Sharing a preview with someone who has no Pocket ID
Every slot is behind the Pocket ID gate by default. A custom slot can instead get basic auth or no auth at all;
that is configured on the host, not here (stacks/edge/proxy/nginx.conf.template in the homelab repo, blocks 2 and
3, plus a line in preview.htpasswd for basic auth). Then label the PR slot:<name>.
Inputs
| input | default | |
|---|---|---|
name |
repo name | the preview's identity within the repo; one comment and one container per name |
path |
. |
where to look for the Dockerfile / build output |
install |
auto | command at the repo root, or none |
build |
auto | command at the repo root, or none |
output |
auto | build output dir relative to path |
slot |
pool | pin to a custom slot |
pool |
8 |
pool size; must match the markers on the host |
env |
KEY=VALUE per line for the container; secrets are fine (${{ secrets.X }}) |
|
memory |
512m |
container memory limit |
domain |
preview.rensebakker.nl |
parent domain |
Outputs: url, slot.
Limits
- Previews share a 4-thread box with CI and everything else: use
paths:filters and skip drafts in busy repos. - A preview that needs a database must bring it (a
Dockerfile.previewwith an embedded one, or anenvpointing at a throwaway) — nothing here provisions one. - Forks get no secrets and a read-only token; the action does not try.
- The host side (nginx blocks, the slot markers, the hourly reaper) lives in the homelab repo.