Rulebearing Playground Coverage Schemas GitHub

Releasing

How Rulebearing reaches its four registries and GitHub releases. Decisions: ADR-0020 (one name everywhere, reserved on one day), ADR-0019 (MIT), ADR-0025 (pinned, least-privileged workflows). Plan: wave 0, Step 11.

The 0.0.1 name reservation (wave 0, done once)

Four placeholder packages hold the name rulebearing (Rulebearing on NuGet). Each carries a README saying what the name is reserved for and where the project lives, and nothing else. They are published by hand from the maintainer's own session, on one day, in this order, because a lookup proves a name is free now, not that it stays free.

Order Registry Package source Credential in the session
1 crates.io wrappers/crates/rulebearing/ (outside the workspace; every workspace crate is rb-*) cargo login
2 npm wrappers/npm/ (now the real package; the script refuses to publish it) npm login
3 PyPI wrappers/pip/ (now the real package; the script no longer builds it) TWINE_USERNAME=__token__, TWINE_PASSWORD
4 NuGet wrappers/nuget/ NUGET_API_KEY
wrappers/publish-placeholders.sh --dry-run   # the crates.io dry run; reports the npm, PyPI and NuGet reservations as done
wrappers/publish-placeholders.sh             # refuses at the npm step: the reservations are published, and wrappers/npm is the real package

Then, the same day:

  1. Create the GitHub organisation rulebearing (the web interface only; the API cannot create one).
  2. Open each registry page, confirm it shows 0.0.1 and the reservation README, and record the four URLs and the @rulebearing scope result in the plan's 0E status table.

If a registry rejects the name, stop: ADR-0020 names the reserves (plumbrule, then trussworthy), and switching is a new ADR, not a rename in place.

Registry tokens stay in the maintainer's keychain in wave 0. None is stored in GitHub until wave 1's release workflow publishes the npm wrapper; wave 2 adds PYPI_TOKEN and NUGET_API_KEY (below).

Binaries (from wave 0)

.github/workflows/release.yml builds rulebearing for the six targets of architecture ยง Distribution: Linux x86-64 (glibc and musl) and ARM64, macOS ARM64 and x86-64, Windows x86-64. Each target's archive, rulebearing-<target>.tar.gz, holds the binary and the npm API's addon, rulebearing.node (crates/rb-node; the musl one links the C runtime dynamically, as a shared library must).

Trigger What happens
Manual run always a dry run: builds every target, packs and install-checks every registry's packages, uploads them as workflow artefacts, releases and publishes nothing
Tag with any pre-release suffix, vX.Y.Z-<suffix> (-rc.1, -beta.1, ...) the same dry run, from a tag
Tag vX.Y.Z builds every target and, once the version check and all three registries' install checks have passed, attaches the archives and SHA256SUMS to a GitHub release

The workspace version in Cargo.toml is the release version; tag the commit that carries it. A tag that does not carry it (vX.Y.Z or vX.Y.Z-<suffix> for the workspace's X.Y.Z) fails the version job, and with it the release.

Wrappers (from waves 1 and 2)

The npm wrapper (wave 1) and the NuGet and PyPI wrappers (wave 2) replace the placeholders with packages that carry the platform binary and nothing else (wrappers/README.md). Their publishing steps are in release.yml, so one tag releases the binary and every wrapper at one version (FR-DIST-01, plan 0002 Step 14). A version job reads the workspace version once, checks that a tag carries it, and hands it to every packing job.

Registry Packages Pack Install check (macOS arm64, Linux x64, Windows x64) Publish (a vX.Y.Z tag only) Credential
npm rulebearing (with the programmatic API, rulebearing/vitest and rulebearing/launcher), six rulebearing-cli-<platform>, each with the binary and the API's addon, eslint-plugin-rulebearing, and the rule library rulebearing-rules npm-pack npm-install-check npm-publish, with provenance trusted publishing per package, NPM_TOKEN for a package with no trust setting yet; npm-credentials and npm-publish hold id-token: write (ADR-0069)
PyPI six rulebearing wheels, pytest-rulebearing, the rule library rulebearing-rules (wheel and source distribution) pypi-build pypi-install-check pypi-publish, with twine PYPI_TOKEN; contents: read
NuGet Rulebearing (dotnet tool), and what wrappers/nuget/pack.sh packs, the rule library Rulebearing.Rules among them nuget-pack nuget-install-check nuget-publish, with dotnet nuget push trusted publishing through NuGet/login with NUGET_USER, and NUGET_API_KEY for a new package's first push; the job adds id-token: write (ADR-0068, ADR-0070)

The GitHub release job needs the version job and all three install checks, and each publish job needs its install check and the release job, so no release exists and nothing is published unless every registry's packages installed and ran, and nothing is published before the release exists. Each install check installs from the packages the run built alone (npm install of the tarballs, pip install --no-index --find-links, and dotnet tool install with a nuget.config that clears every other source), so a rerun after a publish still checks what it built, not the registry's copy. A dry run packs and install-checks all three registries and publishes nothing. Each publish step skips a version already on the registry (npm view, twine --skip-existing, --skip-duplicate), so rerunning a partly published release finishes it.

The npm wrapper (from wave 1)

wrappers/npm/ is the rulebearing package: a launcher that runs the binary, and dependency-cruiser's programmatic API over the addon rulebearing.node (plan 0003 Step 22, ADR-0062), both from one of six unscoped platform packages, rulebearing-cli-<platform>, listed under optionalDependencies (plan 0001 Step 19, ADR-0020; the @rulebearing scope is not held). The committed package.json carries the workspace version; the release version is stamped at staging time and the source tree is never edited.

release.yml adds three jobs after binaries:

Job Runs on What it does
npm-pack every trigger checks that a tag carries the workspace version, builds the launcher, runs wrappers/npm/scripts/stage.mjs over the six archives, builds and stages eslint-plugin-rulebearing with the version stamped into it and its rulebearing dependency (plan 0002 Step 13), packs the eight packages with npm pack, and packs the rule library rulebearing-rules with wrappers/rules/pack.sh, into the npm-packages artefact
npm-install-check every trigger on macOS arm64, Linux x64 and Windows x64, installs rulebearing and the host's platform package from those tarballs into an empty project and asserts that npx rulebearing --version prints rulebearing <version>, that rulebearing/vitest loads, and that the API's cruise() reads a two-file project through the platform package's addon; then that extends: rulebearing-rules/nextjs resolves once the library's tarball is installed (the rule library)
npm-publish a vX.Y.Z tag only, after release and the install check npm publish --provenance --access public for the six platform packages, then rulebearing, then eslint-plugin-rulebearing, then rulebearing-rules, from the same tarballs

A dry run (a manual run, or a tag with a pre-release suffix) therefore packs and install-checks exactly what a release would publish, and publishes nothing. npm-publish and npm-credentials hold id-token: write; every other job keeps contents: read (ADR-0025).

npm-publish uses trusted publishing (ADR-0069). It installs npm 11.21.0, because trusted publishing needs 11.5.1 or later and Node 22 ships npm 10. For each package whose trust setting on npm names benbahrenburg/rulebearing and release.yml, npm exchanges the job's OIDC token for a publish token; --provenance signs with the same OIDC token. A package with no trust setting falls back to the NPM_TOKEN repository secret. Only rulebearing exists on npm, and its trust setting was made on 2026-10-10, so the first release creates the six platform packages and eslint-plugin-rulebearing with NPM_TOKEN. That token needs read and write on all packages and must bypass 2FA, or the publish fails with EOTP, as the v0.1.0 tag's did. npm ends direct publishing for tokens that bypass 2FA in January 2027, so the first release has to come before then (ADR-0069). The CLI creates one:

npm token create --name rulebearing-first-release --packages-all \
  --packages-and-scopes-permission read-write --bypass-2fa --expires 30
pbpaste | gh secret set NPM_TOKEN -R benbahrenburg/rulebearing   # after copying the printed token

After that release, the maintainer gives the seven new packages the same trust setting from the CLI, deletes the secret and revokes the token:

npm login   # 2FA must be on: npm profile get "two-factor auth"
for p in eslint-plugin-rulebearing rulebearing-cli-darwin-arm64 rulebearing-cli-darwin-x64 \
         rulebearing-cli-linux-arm64-gnu rulebearing-cli-linux-x64-gnu \
         rulebearing-cli-linux-x64-musl rulebearing-cli-win32-x64-msvc; do
  npm trust github "$p" --repo benbahrenburg/rulebearing --file release.yml --allow-publish --yes
  sleep 2
done
npm trust list rulebearing
gh secret delete NPM_TOKEN -R benbahrenburg/rulebearing
npm token list   # then npm token revoke <id>

A manual run of the release workflow proves the trust setting without publishing: its npm-credentials job republishes the rulebearing 0.0.1 tarball already on npm with no token configured and provenance off. The registry refuses it as already published only when npm exchanged the OIDC token, which needs a matching trust setting; without one npm has no credential and fails with ENEEDAUTH, which fails the job. The same job checks NPM_TOKEN with npm whoami when the secret is set. Renaming release.yml or moving the repository needs every package's trust setting replaced (npm trust revoke, then npm trust github); npm keeps one per package.

To stage and install the packages by hand on one machine, without publishing:

cargo build --release -p rb-cli
mkdir -p /tmp/rb/dist /tmp/rb/content && cp target/release/rulebearing LICENSE README.md /tmp/rb/content/
tar -czf /tmp/rb/dist/rulebearing-aarch64-apple-darwin.tar.gz -C /tmp/rb/content .   # the host's target
(cd wrappers/npm && npm run stage -- /tmp/rb/dist 0.0.1 /tmp/rb/staged --partial)
npm pack /tmp/rb/staged/rulebearing /tmp/rb/staged/rulebearing-cli-darwin-arm64 --pack-destination /tmp/rb
(mkdir -p /tmp/rb/project && cd /tmp/rb/project && npm init -y && npm install /tmp/rb/*.tgz && npx rulebearing --version)

The install check also imports rulebearing/vitest from the installed package, with vitest@5 as its peer, and checks that defineArchitectureTests and RulebearingReporter are exported (adapters/vitest).

PyPI (from wave 2)

wrappers/pip/ is the rulebearing project: one wheel per release target, each with that target's binary in the package data and a console script rulebearing. adapters/python/pytest-rulebearing/ is the pytest-rulebearing project, a pure-Python wheel that depends on exactly rulebearing==<version>. Both stamp the release version at build time through a hatchling hook; the committed metadata carries none, and a semantic pre-release is written as PEP 440 (0.2.0-rc.1 is 0.2.0rc1).

Job What it does
pypi-build runs wrappers/pip/scripts/build-wheels.sh over the six archives with pinned build, hatchling, packaging and twine; asserts six rulebearing wheels; builds the rulebearing-rules wheel and source distribution with wrappers/rules/pack.sh; twine check --strict; uploads the pypi-packages artefact
pypi-install-check installs rulebearing from the built wheels only (--no-index) and pytest-rulebearing from them (pytest from PyPI) into a new venv, asserts rulebearing --version prints rulebearing <version>, then runs pytest with rulebearing = true over adapters/fixture and asserts three failures, two passes and the liveness reason; then that extends: rulebearing-rules/nextjs resolves once the library's wheel is installed
pypi-publish twine upload --skip-existing, the rulebearing wheels first, then pytest-rulebearing, then rulebearing-rules

The manylinux tag of a *-linux-gnu wheel is the newest GLIBC_2.N symbol version the binary needs, read with readelf, so a wheel never claims a glibc older than the binary requires. The musl build is static and is tagged musllinux_1_1_x86_64; the macOS tags are Rust's default deployment targets (11.0 for arm64, 10.12 for x64).

PYPI_TOKEN is a PyPI API token, stored as a repository secret (set 2026-09-25). PyPI scopes a token only to projects that already exist, and pytest-rulebearing does not exist until its first upload, so the first release needs either a token for the whole account or pytest-rulebearing reserved first; after that release, replace it with a token scoped to the two projects. A manual run of the release workflow proves the token without publishing: its pypi-credentials job re-uploads the rulebearing 0.0.1 wheel already on PyPI with --skip-existing, which PyPI refuses before reading the file when the token is missing, revoked or scoped to other projects, and warns while pytest-rulebearing is not on PyPI. Trusted publishing would need id-token: write on pypi-publish and a third-party action pinned by commit; it can replace the token in a later change without touching the other jobs.

To build and install the wheels by hand on one machine, without publishing (the archive from the npm example above):

pip install build==1.6.1 hatchling==1.32.4 packaging==26.3
PYTHON=python wrappers/pip/scripts/build-wheels.sh /tmp/rb/dist 0.1.0 /tmp/rb/pypi --partial
python -m venv /tmp/rb/venv && /tmp/rb/venv/bin/pip install --no-index --find-links /tmp/rb/pypi rulebearing
/tmp/rb/venv/bin/rulebearing --version

pytest wrappers/pip --cov=wrappers/pip does the same for the host through tests/test_wheel_smoke.py.

NuGet (from wave 2)

The Rulebearing dotnet tool, the Rulebearing.TestAdapter packages and the Rulebearing.Analyzer package are built under wrappers/nuget/, adapters/dotnet/ and frontends/Rulebearing.Analyzer/ (plan 0002 Step 14, plan 0003 Step 21). release.yml depends on one entry point there, and on nothing else in the .NET tree:

wrappers/nuget/pack.sh <dist-dir> <version> <out-dir>
Contract
Input <dist-dir> holds the six rulebearing-<target>.tar.gz archives the binaries job builds; <version> is the workspace version
Output every .nupkg to publish, in <out-dir>, at <version>: Rulebearing.<version>.nupkg, the dotnet tool with the binaries under runtimes/<rid>/native/, the seven Rulebearing.TestAdapter packages (the core and its xUnit, xUnit v3, NUnit, MSTest v2, MSTest v4 and TUnit packages), Rulebearing.Analyzer, and the rule library Rulebearing.Rules (wrappers/rules)
Environment the .NET 10 SDK on ubuntu-latest; no network beyond NuGet restore; no credentials
Check dotnet tool install Rulebearing --version <version> --tool-path <dir> --add-source <out-dir>, then <dir>/rulebearing --version prints rulebearing <version> on macOS arm64, Linux x64 and Windows x64

nuget-pack fails with a named error when pack.sh is absent or writes no Rulebearing.<version>.nupkg or Rulebearing.Rules.<version>.nupkg. nuget-install-check also builds a class library against the built Rulebearing.Analyzer alone, with a rule its one class breaks, and fails unless the build fails with RB0002, and restores Rulebearing.Rules into an empty global packages folder, where extends: rulebearing-rules/nextjs must then resolve. nuget-publish pushes every package in the artefact with --skip-duplicate. nuget-publish uses trusted publishing (ADR-0068). A policy on nuget.org, created 2026-10-10 under the package owner bencoding, trusts release.yml in benbahrenburg/rulebearing with no environment, with the scope "push new packages and package versions" and the glob Rulebearing*. The glob has to cover every id the release packs: only Rulebearing exists on nuget.org (the 0.0.1 reservation), so the first release creates the seven Rulebearing.TestAdapter* packages and Rulebearing.Analyzer, and a policy limited to existing packages would push Rulebearing and then fail. The job runs NuGet/login with NUGET_USER, a repository secret holding the account name bencoding, and pushes with the one-hour key it returns; it holds id-token: write for that exchange. A manual run of the release workflow proves the policy without publishing: its nuget-credentials job makes the same exchange and pushes the Rulebearing 0.0.1 package already on nuget.org with --skip-duplicate, which fails when no policy matches, NUGET_USER names the wrong account, or the key's scope does not cover Rulebearing. The policy's key cannot create a package ID that does not exist yet: nuget.org applies a policy only to packages its owner already owns, and refused Rulebearing.Analyzer with 403 in the v0.1.1 release although the glob matched. So a push that fails with 403 is retried with the NUGET_API_KEY repository secret (set 2026-09-25; scope "push new packages and package versions", glob Rulebearing*), which is read only for a package's first push and is kept for that (ADR-0070). Renaming release.yml, moving the repository or adding an environment to nuget-publish needs the policy edited to match.

The rule library (from wave 3)

The rule library is the framework presets of presets/frameworks/ and presets/rulebearing/recommended.yaml, the files the binary bundles as rulebearing:<name>, published as packages so a repository can pin the rules' version through its package manager and write extends: rulebearing-rules/<name> (ADR-0071, FR-REACH-04, plan 0003 Step 23). It ships from this repository with every release, from the same tag and at the release version; its changelog is the release notes, which list the presets each release changed. wrappers/rules/ holds the three package definitions and no copy of the presets; wrappers/rules/pack.sh packs them from presets/ at a version given on the command line, so nothing in the source tree is edited:

wrappers/rules/pack.sh <version> <out-dir> [npm] [nuget] [pypi]   # none named: all three
Registry Package Packed by Installed layout, where extends finds it Install check (macOS arm64, Linux x64, Windows x64) Publish
npm rulebearing-rules, the presets at the package root npm-pack (pack.sh npm), in the npm-packages artefact node_modules/rulebearing-rules/<name>.yaml npm-install-check: npm install of the built tarball npm-publish, last, with provenance
PyPI rulebearing-rules, a wheel and a source distribution, the presets in the rulebearing_rules package pypi-build (pack.sh pypi), in the pypi-packages artefact; twine check --strict site-packages/rulebearing_rules/<name>.yaml pypi-install-check: pip install --no-index --find-links into the check's virtual environment pypi-publish, twine upload --skip-existing
NuGet Rulebearing.Rules, a content package with no assembly, the presets under rulebearing-rules/ nuget-pack, through wrappers/nuget/pack.sh (pack.sh nuget), in the nuget-packages artefact <global packages>/rulebearing.rules/<version>/rulebearing-rules/<name>.yaml nuget-install-check: dotnet restore of a project that references it, with a nuget.config that clears every other source nuget-publish, with the other packages

Each install check writes a configuration that is only extends: rulebearing-rules/nextjs and runs rulebearing test twice with the binary that registry installed: before the library is installed it must exit 3 naming the missing package, and after it must run the preset's examples with none failing. So a check proves the preset came from the package the run built, not from the binary's bundled copy or a registry. A dry run packs and install-checks the library with everything else and publishes nothing. pytest wrappers/rules --cov=wrappers/rules does the same on one machine with the binary built from the checkout, and CI's guards job runs rulebearing test over every preset (--require-comment-token for the framework presets).

Publishing uses the credentials the other packages use:

Registry Credential
NuGet the trusted-publishing policy, whose glob Rulebearing* covers Rulebearing.Rules; a package ID's first push falls back to NUGET_API_KEY (ADR-0070)
PyPI PYPI_TOKEN, which must cover the account while rulebearing-rules does not exist; pypi-credentials warns until it does
npm trusted publishing, after one first publish from the maintainer's session (below); npm-credentials warns until the package exists

npm cannot set trust on a package that does not exist. Unless NPM_TOKEN is set, the first release's npm-publish therefore publishes every other package, then fails at rulebearing-rules with an error that says so. The maintainer then publishes that one tarball, from the same run's artefact, from a session with 2FA, gives the package the trust setting, and reruns the job, which skips everything already published:

gh run download <run-id> -R benbahrenburg/rulebearing -n npm-packages -D /tmp/rb-npm   # the tag's release run
npm login   # 2FA must be on: npm profile get "two-factor auth"
npm publish /tmp/rb-npm/rulebearing-rules-X.Y.Z.tgz --access public
npm trust github rulebearing-rules --repo benbahrenburg/rulebearing --file release.yml --allow-publish --yes
npm trust list rulebearing-rules
gh run rerun <run-id> -R benbahrenburg/rulebearing --failed

That first version has no provenance statement, because it was not published from the workflow; every later one has. Once the first release has published the library on all three registries, the separate benbahrenburg/rulebearing-rules repository, which never published, is archived (ADR-0071).

The docs site and the schemas on Pages (from wave 4)

The graph document's and the configuration's JSON schemas carry $ids under https://benbahrenburg.github.io/rulebearing/schema/ (schema/README.md), and rulebearing import writes that URL into every configuration it produces. Since wave 4 they are served by the docs site, which is the only thing deployed to the repository's Pages site (ADR-0072). From wave 1 to wave 4, release.yml's schema-pages job served them alone; that job is removed.

Workflow and job Runs on What it does
docs.yml, docs-site a pull request touching the site's inputs; a push to main; a manual run; the completion of a release.yml run started by a vX.Y.Z tag push builds the site with site/build.sh: rb-wasm for the playground, a page for every tracked document under docs/ and the root guides, and schema/v1.json and schema/config-v1.json read from the tag of the latest published release (gh release view), never from the checkout. Then the link check over the built site, the npm licence check, and the playground's browser test, which drives all five rb-wasm exports through the page (evaluate, cruise_ts, cruise_py, explain, version) and compares their output with the command line's committed output
docs.yml, docs-deploy after docs-site, on main only, never for a pull request deploys the built site with actions/deploy-pages; the job alone holds pages: write and id-token: write (ADR-0025)

The schema URLs hold the released contract, not the state of main, while the pages follow main. A release changes the schemas, and its completion starts docs.yml, which deploys the site from main with the new tag's schemas. A workflow_run runs in the default branch's context, so that deploy is a main deploy too.

Two repository settings are needed once: Pages with the source "GitHub Actions", and a deployment rule on the github-pages environment that admits main. The rule admitting v* tags, which schema-pages needed, is no longer used.

After a deploy, check:

URL Expect
https://benbahrenburg.github.io/rulebearing/ the README as the home page, with the sidebar
https://benbahrenburg.github.io/rulebearing/playground.html "Ready: rulebearing X.Y.Z." under the playground; Run prints the no-circular guard's finding; A TypeScript repository then Run prints cruise_ts's report on rb-wasm's fixture, and Explain the named rule
https://benbahrenburg.github.io/rulebearing/schema/v1.json the graph document schema of the latest release, $id equal to the URL
https://benbahrenburg.github.io/rulebearing/schema/config-v1.json the configuration schema of the latest release, $id equal to the URL

To build and serve the site locally:

npm ci && site/build.sh                # target/site; RB_SCHEMA_REF=vX.Y.Z picks the schemas' tag
node site/dist/node/main.js serve target/site --port 8080
npm --prefix site run test:e2e         # the playground in headless Chromium (npx playwright install chromium-headless-shell once)