A collection of reusable workflows for GitHub Actions.
Private callers always use self-hosted runners. github_hosted_runner remains accepted but cannot override repository visibility. A private caller's runs_on must be a JSON array containing self-hosted, for example '["self-hosted","Linux","large"]'; other valid JSON selections fall back to the workflow's self-hosted defaults. Invalid JSON can fail expression evaluation. Keep any required OS, size, or host labels in that array.
Public callers default to ubuntu-latest and may override runs_on. Unknown visibility uses self-hosted defaults. bun run test evaluates the workflow expressions with GitHub's expression engine to check this policy; CI runs it alongside actionlint.
The four install-capable workflows (test.yml, deploy.yml, release.yml, run-script.yml) declare these optional secrets:
VERDACCIO_TOKEN: auth token for the private Verdaccio registry (@willbooster-private/*). When set, the workflow generates git-excluded workspace registry configs before installing dependencies —.npmrcfor npm/bun/yarn1 and.yarnrc.ymlfor Yarn Berry (which ignores.npmrc), so Berry consumers no longer need to reference${VERDACCIO_TOKEN}from their committed.yarnrc.yml. Every caller should pass it. The generated configs stay on disk for the whole job, but the secret itself is step-scoped:${VERDACCIO_TOKEN}only expands in the "Install dependencies" step of all four workflows, pluscommon/ci-setupand "Deploy" indeploy.yml,common/ci-setupand "Run script" inrun-script.yml, "Release" inrelease.yml, and "Test release script" intest.yml. Consumer scripts running on any other step (e.g.test/ci-setup,cleanup,build, tests) see an emptyVERDACCIO_TOKENand must rely on the already-installed dependency graph instead of fetching from the private registry at run time.TAKUMI_GUARD_TOKEN: auth token for the Takumi Guard registry proxy, which blocks known-malicious packages. When set, the generated registry configs (.npmrcfor npm/bun/yarn1,.yarnrc.ymlfor Yarn Berry) route newly resolved public (default-registry) packages throughhttps://npm.flatt.tech/for every package manager;@willbooster-private/*packages keep resolving from Verdaccio withVERDACCIO_TOKEN. The token follows the same step-scoping asVERDACCIO_TOKEN. While either registry token is present, the generated.yarnrc.ymlreplaces the committed top-levelplugins:,npmRegistryServer:,npmRegistries:, andnpmScopes:blocks for the whole job, and the generated.npmrcreplaces the managedregistry=/scope lines: consumers must not rely on committed default-registry overrides, extra scopes, or third-party Berry plugins during CI. Independently of this secret, WillBooster's self-hosted runner machines carry a persistent~/.npmrc(and~/.yarnrc.yml) with Guard's public anonymous token, provisioned by self-host-utils, so default-registry installs on those machines route through the proxy even for callers that pass no token. Coverage: for bun, an EMPTYresolvedfield does not bypass the proxy — bun derives the download URL from the configured registry, so already-locked packages go through Guard too. A non-emptyresolvedis what bypasses it, and Yarn 1 always records one; Yarn Berry resolves through the proxy via the generated.yarnrc.yml(itsyarn.lockstores registry-agnosticnpm:resolutions, so no lockfile normalization is needed). Note the side effect for bun consumers: an install that UPDATESbun.lockunder any Guard-routed.npmrc(workspace or machine-level) makes bun rewrite every already-locked package'sresolvedto the proxy host, sotest.ymlnormalizes those URLs back to""regardless of whether this token is set — otherwise a committed lockfile would pin every environment to the proxy and fail cold-cache installs with 401 for anyone holding aregistry.npmjs.orgtoken. The normalization runs before the diff is reported, so it appears with the other fixer changes intest.yml's failure output. Publishing repositories must declarepublishConfig.registryexplicitly (npmjs for public packages — wbfy injects it; Verdaccio publishers already declare theirs): without it,npm publishresolves the Guard proxy from the generated workspace.npmrc— or from the machine-level~/.npmrcon tokenless self-hosted runs — and fails with 405 (Berry publishers likewise neednpmPublishRegistryorpublishConfig.registry, sinceyarn npm publishfalls back tonpmRegistryServer).NPM_TOKEN(declared byrelease.ymlonly): auth token for registry.npmjs.org, for packages that must token-publish — e.g. the first publishes of napi platform packages, which npm trusted publishing cannot create. It is step-scoped to the "Release" step and wins overVERDACCIO_TOKENas theNPM_TOKENenv var there. It authenticates semantic-release's npm plugin (which writes its own--userconfignpmrc); a consumer release script runningnpm publishdirectly must write its own npm auth config from the env var, because the generated workspace.npmrccarries no registry.npmjs.org line. Use a granular, package-scoped, short-lived publish token and revoke it once trusted publishing takes over.FNOX_AGE_KEY: age secret key that decrypts the age-encrypted secrets committed in the caller'sfnox.toml. Required for repositories whosefnox.tomlcontains age-encrypted secrets; after mise installs fnox,deploy.yml,release.yml, andrun-script.ymlfail fast when the committed secrets cannot be resolved (missing or wrong key), whiletest.ymlonly warns (fork pull requests run it without secrets). Afnox.tomlwith only plaintext defaults needs no key, but a non-development job must still declare the selected profile (an inline[profiles.<name>]or afnox.<name>.tomlfile) — an undeclared profile silently falls back to the base (development) secrets, so the check rejects it.
Do NOT pass these secrets explicitly to the other workflows (e.g. semantic-pr.yml, close-comment.yml): GitHub rejects a secrets: map entry the callee does not declare (secrets: inherit is exempt from this validation). Running wbfy (>= 3.0.0) on the caller repository injects VERDACCIO_TOKEN/FNOX_AGE_KEY automatically.
test.yml never pushes fixes. Its test job runs the pull request's own dependencies, so its checkout persists no push credential (persist-credentials: false); when the fixers (gen-code, cleanup, lint-fix, build) change anything, the job fails with the diff, and a developer or agent applies it. The former App-based autofix-apply.yml flow (a workflow_run job committing an uploaded patch with a GitHub App token) was retired in 2026-07: committing CI-generated patches with a bot credential is a standing risk with little benefit now that agents fix and verify before pushing, and retiring it let the App private key and its token broker be decommissioned. The autofix.ci path for public repositories (autofix.yml) and the nightly self-applying wbfy.yml were retired at the same time, so this repository hosts no workflow that writes to a caller's repository at all.
The test.yml caller's permissions must NOT be narrowed just because test.yml never pushes or dispatches; the grants are needed for other reasons:
contents: write— the "Test release script" step runssemantic-release --dry-run, which performs agit push --dry-runto verify write access and aborts withEGITNOPERMISSIONwithout it, even in a dry run. Callers such asprompt-study,build-tsandagentic-workflowsreach that step.actions: write— the test job runsfkirc/skip-duplicate-actionswithcancel_others: true, which callscancelWorkflowRun. Dropping the grant also loses the impliedactions: read, so duplicate/paths_ignoreskipping silently stops working too.
Source checkouts use the automatically provided GITHUB_TOKEN (no secret needs to be passed, and self-hosted runners need no SSH deploy key). Exception: sync.yml pushes to the caller-supplied DEST_GIT_URL secret, so an SSH-form value there still needs runner SSH credentials until it is migrated to an HTTPS token URL.
Note: this repository is mirrored to WillBoosterLab/reusable-workflows with one-way-git-sync via the sync script, which maintainers run from their machines (bun run sync; renovate.jsonc and node_modules are excluded). The mirror is not synced automatically on merge, so it can lag main — run bun run sync after merging changes that WillBoosterLab callers need. The script passes -b main explicitly rather than relying on inference, and the pinned one-way-git-sync must stay at >= 6.0.45: earlier versions turned a transient clone failure into a push to a branch literally named undefined while still reporting success (and, with -b, into fatal: a branch named 'main' already exists). Verify the result by the mirror's own sync commit rather than the exit code — its title embeds the source commit it mirrored, so it should name the local main HEAD:
git rev-parse main
gh api repos/WillBoosterLab/reusable-workflows/commits/main --jq '.commit.message | split("\n")[0]'The test workflow streams test output while preserving the test exit status. Local workflow log capture and upload are enabled together. Set upload_test_log: true only when test output contains no secrets: artifact files do not receive GitHub's console secret masking. This opt-in retains logs for 14 days after success or failure, with test-output-<OS>-<Node>-<check-run-id>-<attempt> names. Configured artifact_path uploads also run after failures, using matching test-artifact-<OS>-<Node>-<check-run-id>-<attempt> names. Node is pinned for a repository-managed version; the check-run ID distinguishes jobs and the attempt distinguishes reruns.