Shared CI, deploy and repo config for Astro sites on Cloudflare Workers or Netlify, and for bun tools and libraries. Defaults assume bun, Astro and Playwright; pnpm is supported.
| Piece | What it does |
|---|---|
site-ci.yml |
Lint, typecheck, build, unit, license, Playwright, axe and Unlighthouse for sites |
tool-ci.yml |
gitleaks, lint, typecheck, build, test, workflow audit for bun tools |
resolve-env.yml, deploy actions |
Map branch to environment, deploy to Cloudflare or Netlify |
unlighthouse-runner, check-licenses |
Performance budgets and copyleft license check (npm bins) |
@taraxvoid/voidflow/playwright |
Playwright config for astro preview |
@taraxvoid/voidflow/biome |
Shared Biome config |
github>taraxvoid/voidflow |
Shared Renovate preset |
lefthook/base.yml |
Shared git hooks |
Pin to a release commit SHA with the version in a comment, like any third-party action. Renovate and Dependabot can bump it.
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@<sha> # vX.Y.Z@v0 follows the newest stable 0.x release if you'd rather float. To try
unreleased changes, pin the SHA of a vX.Y.Z-next.N tag. Don't use @main or
@next. Release channels are described in RELEASING.md.
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, next, live]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
jobs:
validate:
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@<sha> # vX.Y.ZIt runs whichever of these package.json scripts the site defines (missing
ones are skipped with a notice):
| Script | When |
|---|---|
lint, check, build, test:unit |
always |
check:licenses |
lockfile or scripts/ changed |
test:e2e:full |
PRs into main |
test:e2e:all, test:e2e:a11y |
PRs into next and live |
test:e2e:lighthouse |
PRs into live |
Inputs:
| Input | Default | |
|---|---|---|
runner |
ubuntu-latest |
Runner label |
package-manager |
bun |
bun or pnpm. pnpm version comes from packageManager, Node from engines |
single-environment |
false |
See below |
build-env |
{} |
JSON object of build env vars. Visible in logs, so no secrets |
e2e-extra-paths |
["wrangler.jsonc", "site.vars.json"] |
JSON array of globs that also trigger e2e |
socket-scanner |
false |
Run the bun install security scanner in CI (see below) |
playwright-install-with-deps |
true |
Install Playwright system deps |
For sites where main deploys to prod and PRs get previews (no next/live),
pass single-environment: true and add a push trigger so direct pushes to
main are validated:
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
jobs:
validate:
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@<sha> # vX.Y.Z
with:
single-environment: truemain then runs the production tier (test:e2e:all, test:e2e:a11y,
test:e2e:lighthouse).
If bunfig.toml sets install.security.scanner, CI installs with that line
removed (everything else still applies) and leaves a notice, so a flaky
third-party API can't fail the build. Local installs still run it. Pass
socket-scanner: true to run it in CI too.
Write it as a plain scanner = "..." line or dotted security.scanner. An
inline table can't be removed cleanly, so the install fails instead.
Workflows that install outside site-ci.yml get the same behavior from the
composite action:
- uses: oven-sh/setup-bun@<sha> # <version>
- uses: taraxvoid/voidflow/actions/bun-install@<sha> # vX.Y.Z
# with: { socket-scanner: 'true' }For bun CLIs, TUIs and libraries. Runs gitleaks over full history, installs
(same scanner handling), installs tools from mise.toml if present, then
whichever of lint, check, build and test exist, plus check:licenses
when dependencies or scripts/ change. When workflows change it also runs
jactionlint and zizmor. Bun version
comes from .bun-version if present. Inputs: runner, socket-scanner.
name: CI
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pull-requests: read
jobs:
validate:
uses: taraxvoid/voidflow/.github/workflows/tool-ci.yml@<sha> # vX.Y.ZThe required status check is validate / Validate.
resolve-env.yml maps the branch to an environment: main to dev, next
to staging, live to prod. With single-environment: true, main maps to
prod, pull requests to preview, and any other ref fails. It's a separate
job so the deploy job's name: can show the environment.
jobs:
resolve-env:
needs: validate
if: github.event_name == 'push'
uses: taraxvoid/voidflow/.github/workflows/resolve-env.yml@<sha> # vX.Y.Z
deploy:
name: Deploy [${{ needs.resolve-env.outputs.env }}]
needs: resolve-env
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: ${{ needs.resolve-env.outputs.env }}
steps:
- uses: actions/checkout@<sha> # <version>
- uses: taraxvoid/voidflow/actions/cloudflare-deploy@<sha> # vX.Y.Z
with:
env: ${{ needs.resolve-env.outputs.env }}
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}actions/cloudflare-deploy:wrangler deployfor the environment, with an optional D1 migration first (migrate-d1). Other inputs:cloudflare-account-id,working-directory,command.actions/netlify-deploy:prodis a production deploy,dev/stagingare alias deploys. Inputs:env,publish-dir,netlify-auth-token,netlify-site-id,working-directory,github-token. Turn off Netlify's own GitHub auto-deploy for the site, or the two will double-deploy.actions/branch-env-map: the mapping step on its own, for use inside a job.
bun add -d @taraxvoid/voidflow # or: pnpm add -D @taraxvoid/voidflowAlso published unscoped as voidflow with identical contents.
Call the bins from package.json scripts rather than bunx --package ..., so
the version stays in the lockfile and the site's bunfig.toml applies. Both
bins need bun on PATH, even in npm or pnpm projects.
"test:e2e:lighthouse": "bun run build && unlighthouse-runner --dir dist"Serves the built directory and runs Unlighthouse with Playwright's Chromium. Fails if a category budget fails.
--dir: defaultdist; usedist/clientfor the Cloudflare adapter--config: defaultunlighthouse.config.ts; start from the example--version: Unlighthouse version
Add .unlighthouse/ to .gitignore.
"check:licenses": "check-licenses"Fails on GPL, AGPL, SSPL and similar copyleft licenses. LGPL is allowed.
For Astro sites that run e2e against astro preview. Requires
@playwright/test. Runs on Node 22+ without Bun.
// playwright.config.js
import { definePreviewConfig } from '@taraxvoid/voidflow/playwright'
export default definePreviewConfig({ ciPort: 4141 })Picks a free port locally and skips webServer when PLAYWRIGHT_BASE_URL is
set. Options: ciPort (required in CI), runner (default bun), timeout
(default 30000), projects (default Pixel 7 and Desktop Chrome), testDir
(default ./test/e2e).
Needs Biome 2. In the site's biome.json:
{
"$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
"extends": ["@taraxvoid/voidflow/biome"],
"files": {
"includes": ["**", "!**/bun.lock", "!public/admin/**"]
}
}The base sets formatting (4-space indent, width 80, single quotes, semicolons
as needed), the recommended rules, and turns off unused-import/variable rules
in .astro files. It has no ignore lists. Lists like files.includes and
overrides merge with the base; single values in the site win.
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>taraxvoid/voidflow"]
}Requires the Renovate GitHub App. Based on config:best-practices: 3-day
release age (except voidflow and security fixes), chore(deps): commits,
grouped updates, Monday mornings. It also bumps the lefthook remote ref, and
justfile tool versions annotated with
# renovate: datasource=... depName=... on the line above.
Shared lefthook hooks for bun or pnpm repos. Each step runs the repo's own
package.json script and is skipped if the script is missing.
pre-commit: dependency guard,format(re-staged),lint:actions,check,test:unit, andcheck:licenses/auditwhen the lockfile changedcommit-msg: Conventional Commits hint (never blocks)pre-push: stop if the remote is ahead, thentest:push
# lefthook.yml
remotes:
- git_url: https://github.com/taraxvoid/voidflow
ref: vX.Y.Z # a tag, not a SHA
configs:
- lefthook/base.ymlInstall the binaries with mise:
# mise.toml
[tools]
lefthook = "2.2.1"
gitleaks = "8.30.1"
jactionlint = "2.1.0"and wire the hooks on install without failing where lefthook is absent (CI):
"prepare": "lefthook install || echo 'lefthook not installed (run mise install); git hooks skipped'"To run less locally, define test:unit:precommit or check:precommit;
pre-commit runs those instead. CI still runs the full scripts. For example:
- bun:
"test:unit:precommit": "bun test --changed" - pnpm workspace:
pnpm -r --filter '...[HEAD]' run <script>(add--filter '!{.}'if the root defines the same script recursively)
Your lefthook.yml can add commands but not override shared ones. Don't set
parallel: true on pre-commit; it applies to the shared steps too and breaks
their ordering.
Upgrading from lefthook/site.yml (v0.13 and earlier): change configs to
lefthook/base.yml. The old file now fails commits with that instruction.
taraxvoid.net, Queer Omaha, Synth Omaha, Soundry, ravenflight.io.
Optional badge for your README (follows light/dark mode):
<a href="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/taraxvoid/voidflow">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/CI-voidflow-8b8fd9?labelColor=2b2d3a&style=flat">
<img alt="CI: voidflow" src="https://img.shields.io/badge/CI-voidflow-5a5fb8?labelColor=3a3d4d&style=flat">
</picture>
</a>See CONTRIBUTING.md. MIT licensed.