@taraxvoid/voidflow
v0.11.1
Published
Shared workflows, actions and tooling for @taraxvoid sites
Maintainers
Readme
voidflow
Shared CI and deploy pipeline for a small fleet of Astro sites on Cloudflare Workers. One set of reusable GitHub Actions workflows and composite actions keeps lint, type checks, license policy, Playwright and axe-core accessibility tests, Unlighthouse budgets, and deploys identical across every site.
- SHA-pinned. Every third-party action in this repo is pinned to a full commit SHA with a version comment.
- Audited. Workflows are checked with zizmor, actionlint, check-jsonschema and shellcheck on every change (see Self-validation).
- In production. It runs the pipelines for the sites listed under Used by, including the marketing site for RavenFlight Industries, LLC.
It is semi-opinionated: the defaults match how my own sites are built (bun, Astro, Playwright).
Used by
- taraxvoid.net, my portfolio
- Queer Omaha, Synth Omaha and Soundry, free community sites for Omaha's queer and music scenes
- ravenflight.io, RavenFlight's marketing site
Usage
Point uses in your configuration at a workflow, e.g. in your .github/workflows/ci.yml. Pin to a release commit SHA with the version in a comment, the same way you would any third-party action (see Versioning).
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@15caea203066d42cb7be021eedb6356fac6ffdec # v0.8.1Non-GitHub / self-hosted runners
Pass runner (defaults to GitHub runner ubuntu-latest)
jobs:
validate:
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@15caea203066d42cb7be021eedb6356fac6ffdec # v0.8.1
with:
runner: my-hosted-runnerSingle-environment sites
Sites with one production environment and no next/live branches (main
deploys to prod, pull requests get previews) pass single-environment: true,
the same flag resolve-env and branch-env-map take:
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> # <version>
with:
single-environment: truemain then gets the production test tier (test:e2e:all, test:e2e:a11y,
test:e2e:lighthouse) instead of only test:e2e:full. Include the push
trigger so a direct push to main is validated too; the e2e steps resolve the
branch from the pushed ref.
pnpm sites, build env, extra e2e paths
jobs:
validate:
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@<sha> # <version>
with:
package-manager: pnpm # default: bun
e2e-extra-paths: '["wrangler.jsonc", "site.vars.json"]' # JSON array of globs
build-env: ${{ format('{{"PUBLIC_SITE_URL":"https://example.com","GIT_SHA":"{0}"}}', github.sha) }}With pnpm, the pnpm version comes from packageManager and Node from volta
or engines in package.json. The lockfile (bun.lock or pnpm-lock.yaml)
follows the package manager. build-env is a JSON object, visible in logs, so
no secrets.
Workflow Types
Validation / Checks
Runs lint, typecheck, check for GPL licenses, e2e with Playwright.
site-ci.yml runs these package.json scripts: lint, check, build,
test:unit, check:licenses, test:e2e:full (PRs into main),
test:e2e:all (PRs into next and live), test:e2e:a11y (next and
live), and test:e2e:lighthouse (live). A script the site does not define
is skipped with a notice, so none of them is required.
Unlighthouse (performance / SEO budgets)
site-ci.yml runs bun run test:e2e:lighthouse on PRs into live. Sites
implement that script with the shared runner here, which serves the built
static directory on a free port and runs
Unlighthouse against it, reusing Playwright's
Chromium. It exits non-zero if a category budget in the site's config fails.
In the site's package.json (pin the tag):
"test:e2e:lighthouse": "bun run build && bunx --package @taraxvoid/[email protected] unlighthouse-runner --dir dist"Options: --dir (default dist, use dist/client for Cloudflare adapter
builds), --config (default unlighthouse.config.ts), --version (pinned
Unlighthouse version). Start from
scripts/unlighthouse/unlighthouse.config.example.ts
and add .unlighthouse/ to the site's .gitignore.
License policy
site-ci.yml runs bun run check:licenses when the lockfile or scripts/
change. Sites implement that script with the shared check, which runs
license-checker in the site's repo root and fails on GPL, AGPL, SSPL and
similar copyleft licenses (LGPL is allowed).
In the site's package.json (pin the tag):
"check:licenses": "bunx --package @taraxvoid/voidflow@<version> check-licenses"Playwright preview config
Shared playwright.config for Astro sites that run e2e against
astro preview. It picks a free port outside CI, sets
ASTRO_PREVIEW_BACKGROUND=false (Astro backgrounds preview in agent
environments, which makes Playwright think the server exited), and drops the
webServer when PLAYWRIGHT_BASE_URL is set.
bun add -d @taraxvoid/voidflow # or: pnpm add -D @taraxvoid/voidflow// playwright.config.js
import { definePreviewConfig } from '@taraxvoid/voidflow/playwright'
export default definePreviewConfig({ ciPort: 4141 })The same release is also published unscoped as voidflow (identical code and
version, with a short pointer README), so bun add -d voidflow and
import ... from 'voidflow/playwright' work too. @taraxvoid/voidflow is the
canonical name.
Options: ciPort (required in CI), runner (default bun, e.g. pnpm),
timeout (default 30000), projects (default Pixel 7 and Desktop Chrome),
testDir (default ./test/e2e). Requires @playwright/test in the site.
Dependency updates (Renovate)
default.json is the shared Renovate preset. Sites add a
renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>taraxvoid/voidflow"]
}It builds on config:best-practices (which pins GitHub Action digests), holds
updates for 3 days like minimumReleaseAge in bunfig.toml (except our own
package and security alerts), uses chore(deps): Conventional Commits, groups
actions, astro, biome and playwright, and runs on Monday mornings. Custom
managers keep three things current that Dependabot cannot see: the
@taraxvoid/[email protected] pin inside package.json scripts, a lefthook
remote's ref tag, and tool versions in a justfile (annotate the line above
with # renovate: datasource=... depName=...). Requires the Renovate GitHub App
on the repo.
||||||| 4041a59
Git hooks (lefthook)
lefthook/site.yml is the shared hook set for the site
stack, consumed as a lefthook remote. It calls the site's own package.json
scripts (bun run --if-present, so a missing script is skipped):
pre-commit: dependency guard,format(formatted files are re-staged),lint:actions,check,test:unit, andcheck:licenses/auditwhen the lockfile changed.commit-msg: advisory Conventional Commits hint (never blocks).pre-push: bail if the remote branch is ahead, thentest:push; skipped when there is nothing new to push.
# lefthook.yml in the site
remotes:
- git_url: https://github.com/taraxvoid/voidflow
ref: v0.10.0 # a tag or branch; lefthook passes it to `git clone --branch`
configs:
- lefthook/site.yml
Lefthook merges lefthook.yml, then remotes, then lefthook-local.yml, so a
site's own lefthook.yml can add commands (for example image compression) but
cannot override ones defined here. Differences go through package.json
scripts: define test:unit:precommit (for example excluding tests that need a
built dist/) and the pre-commit unit step runs it instead of test:unit.
Install the binary through mise (lefthook = "<version>" in mise.toml) and
set "prepare": "lefthook install" so bun install wires the hooks. ref
cannot be a commit SHA; pin a release tag.
Branch rulesets as code
Branch protection for the fleet lives in rulesets/index.ts:
one canonical main / next / live definition (plus prerelease and maintenance
tiers for repos with release channels) and a small per-repo table
(Netlify or not, check names, CODEOWNERS, extra bypass actors). Hand-edited
rulesets drift, and drift is how a next ruleset ends up requiring a check
name that no workflow reports any more.
just rulesets-plan # read-only: diff every repo against the config
just rulesets-plan --repo soundry
just rulesets-plan --check # exit 1 on drift, for CI
just rulesets-apply # create/update rulesets to match
just rulesets-apply --prune # also delete rulesets a repo lists under `retire`Single-environment sites (a stable main that deploys to prod) manage only the
main tier; their old next/live rulesets are listed under retire. voidflow itself
manages main, prerelease (next) and maintenance (release/*) and sets
linearHistory: false so next can be promoted into main with a merge commit.
Uses the gh CLI for auth (needs admin on the repos). Rulesets that are not in
the config are reported and left alone; only retire entries are ever deleted,
and only with --prune. It is repo tooling, not part of the published package.
Deployments
Example deployment job added to your workflow above. resolve-env maps the
branch to dev/staging/prod once, up front, so its output can be used both to
drive the deploy and to label the job (Deploy [dev], Deploy [staging],
Deploy [prod]). A job's name: can reference needs.<job>.outputs.* but
not a step output from within itself, hence the split:
jobs:
resolve-env:
needs: validate
if: github.event_name == 'push'
uses: taraxvoid/voidflow/.github/workflows/resolve-env.yml@15caea203066d42cb7be021eedb6356fac6ffdec # v0.8.1
deploy:
name: Deploy [${{ needs.resolve-env.outputs.env }}]
needs: resolve-env
if: github.event_name == 'push'
environment: ${{ needs.resolve-env.outputs.env }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: taraxvoid/voidflow/actions/cloudflare-deploy@15caea203066d42cb7be021eedb6356fac6ffdec # v0.8.1
with:
env: ${{ needs.resolve-env.outputs.env }}
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}Sites with a single production environment and per-PR previews (no next or
live branch) pass with: { single-environment: true } to resolve-env.yml
(or the same input on actions/branch-env-map). main then maps to prod and
pull_request events map to preview; any other ref fails.
Versioning
Semver, released with release-please
(release-please.yml) on three branch-based channels
(full flow in RELEASING.md):
| Channel | Branch | Example | npm dist-tag |
|---|---|---|---|
| stable | main | 0.10.0 | latest |
| prerelease | next | 0.10.0-next, 0.10.0-next.1 | next |
| maintenance | release/N.x | 0.9.4 | release-N.x |
release-please keeps a standing release PR open on each channel's branch, updated as commits land.
Merging it bumps package.json (and CHANGELOG.md on stable), tags vX.Y.Z,
creates the GitHub release and publishes to npm and JSR under the channel's dist-tag. The PR is
opened with the org's release-bot GitHub App token (RELEASE_BOT_CLIENT_ID /
RELEASE_BOT_APP_PRIVATE_KEY) so CI runs on it.
Feature PRs target next; promotion to main is a merge-commit PR (just promote).
Commit messages should follow Conventional Commits,
enforced loosely, as an advisory commit-msg hint (see
scripts/check-commit-msg.sh), not a blocking check. They
drive the version bump and generated changelog.
Ways to consume a release:
- Floating major tag:
@v0follows the newest stable 0.x release. Easiest to track, and mutable like any floating tag. - SHA pin with a version comment, e.g.
@15caea2... # v0.8.1: what I use. Same hardening as any third-party action, and Renovate or Dependabot can bump it. - Try unreleased changes: pin the commit SHA of a
vX.Y.Z-next/vX.Y.Z-next.Nprerelease tag.
Treat @main and @next as unstable.
Self-validation
This repo validates its own workflows and composite actions (.github/workflows/ci.yml):
- actionlint: workflow schema/logic; also shellchecks
run:steps in.github/workflows/*.ymlautomatically (shellcheckships onubuntu-latest) - check-jsonschema: schema-checks
actions/**/action.ymland the workflow files against SchemaStore'sgithub-action.json/github-workflow.json - shellcheck (
scripts/shellcheck-actions.ts): checksrun:steps insideactions/**/action.yml, which actionlint doesn't reach - zizmor: security audit (unpinned refs, script injection via
${{ }}inrun:, excess permissions, etc.)
Local dev setup
brew install just lefthook actionlint shellcheck bun act uv
lefthook installjust validate: run everything CI runs, locallyjust lint-workflows/lint-actions/lint-shell/security: run one check at a timejust dry-run: full local run ofci.ymlviaact(needs Docker running); pass extra args, e.g.just dry-run -j validation- lefthook runs the fast checks (
lint-workflows,lint-actions,lint-shell) onpre-commit, andzizmoronpre-push
Colima users: .actrc already disables the docker-socket mount (--container-daemon-socket -). act binds /var/run/docker.sock by default, which doesn't exist under Colima and fails with operation not supported. None of these workflow steps need docker-in-docker, so this is safe.
Run just dry-run from a normal checkout, not a linked git worktree. act copies the working tree via docker cp rather than a real clone, so a worktree's .git pointer back to the parent repo doesn't resolve inside the container, so git ls-files-based steps (schema validation, scripts/shellcheck-actions.ts) silently see zero files instead of erroring. Verified clean end-to-end (🏁 Job succeeded) from a plain clone.
Provided as is under the MIT license. Issues and pull requests are welcome.
