What runs on a pull request, what runs on main, and how to run the same things
locally. The workflows are in .github/workflows/; this page names them by
their file.
cargo xtask preflightIt runs, in order: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, the documentation checks, cargo test --workspace,
cargo xtask lean-core-check, cargo xtask holmes-domain-check, a smoke run of
the CLI, and cargo xtask docs-replay, which replays the documented sessions.
It needs Node for that last step. Run it before opening a pull request.
A local pass predicts a CI pass only for the checks that ran. One check here
needs a tool a contributor may not have, and then it says it did not run and
lets preflight continue; in CI (CI=true) the same gap fails the run. So read
the output: a line saying a check was NOT run means CI will run something you
have not.
holmes-domain-check is that check. It builds wesley-holmes-domain for
thumbv7em-none-eabihf, a target with no std, which is how the domain's
no-I/O rule is held. Install the target once and it always runs:
rustup target add thumbv7em-none-eabihf. The workflows install it.
| Workflow | What it runs |
|---|---|
rust-native.yml |
cargo xtask preflight |
ci.yml |
pnpm -w test, a CLI smoke run, and every bats suite under test/*.bats |
preflight.yml |
pnpm run legacy-preflight: the repository hygiene checks listed below |
architecture-boundaries.yml |
Import boundaries of the Node package, and that retired packages stay retired |
docs-link-check.yml |
Relative links in Markdown resolve |
pkg-holmes.yml |
pnpm --filter @wesley/holmes test |
wesley-holmes.yml |
The Holmes assurance run over the schema sets a change touches |
dependency-review.yml |
Fails on a new dependency with a known vulnerability of high severity or worse |
codeql.yml |
CodeQL static analysis |
ci.yml discovers bats suites by glob, so a new test/<name>.bats runs without
editing the workflow. A glob that matches nothing fails the step.
legacy-preflight is scripts/preflight.mjs. It runs, and fails on any of:
- the Git identity guard, and the unit tests under
scripts/*.test.mjs; - ESLint;
actionlintover every workflow;- the Markdown link check;
- markdownlint over every tracked Markdown file but two, named with their
reasons in
.markdownlint-cli2.jsonc; - the forbidden machine-local path check and the package manager policy;
- Prettier, over every tracked file that
.prettierignoredoes not name. The file list comes from Git, so a tracked file under a git-ignored path is checked too; each exclusion in.prettierignoresays why it is there; - dependency-cruiser's import boundaries, and the Apache-2.0 license audit of the workspace packages.
| Workflow | What it does |
|---|---|
cert-shipme.yml |
Produces a SHIPME certificate, only for a push that touches packages/, .github/, or the fixture script |
release-autotag.yml |
Tags the commit if it is the merge of a release/vX.Y.Z pull request |
scorecards.yml |
OpenSSF Scorecard, also on a schedule |
release-crates.yml runs from a release tag. RELEASE.md
describes it and the autotag workflow.
A test executes something and checks what happened. Tests do not search documentation, workflow files, or source code for strings.
-
Rust:
cargo test --workspace.cargo xtask preflightruns it. -
Node:
pnpm -w test. -
bats: the suites under
test/run the CLI, the fixture generators, and the static file server, and check their output. To run them all:pnpm run setup:bats-plugins for f in test/*.bats; do BATS_LIB_PATH=test/vendor bats "$f"; done
batsitself is a prerequisite:brew install bats-core, orapt install bats.setup:bats-pluginsonly verifies the assertion plugins vendored undertest/vendor; it does not install the runner.
The documentation is checked only by executing things:
- Links are followed and must resolve:
cargo xtask docs-check. - The sessions in
README.mdanddocs/getting-started.mdare replayed in a scratch directory against the built CLI:cargo xtask docs-replay, which preflight runs, andtest/docs-examples.batsin CI. What the CLI prints, and the files it writes, are compared with what the page shows. Everywesleycommand a page names, and every option written directly afterwesley, must be one thatwesley --helplists: in a block that is run, in one that is not, and in an inline code span in the prose. Each page must contribute: a page with nothing run or nothing compared fails, and the replay prints how many commands it ran, outputs it compared, and names it checked. docs/cli.mdmust equal what the binary's help prints today, for every command family the root help lists; the same suite checks it.
The replay reads these annotations from the Markdown:
| Annotation | Before | Meaning |
|---|---|---|
<!-- file: NAME --> |
any fenced block | write the block to NAME |
<!-- exit: N --> |
a bash block |
its commands exit N |
<!-- norun: WHY --> |
a bash block |
shown, not run; its command names are still checked |
<!-- shows: NAME --> |
a block not bash |
a contiguous excerpt of NAME, which a command wrote |
<!-- stdout: json-subset --> |
a json block |
every key and value shown is in the real output |
A block takes one annotation, so none can switch another's check off. shows
accepts only a file that a replayed command created or changed: a fixture the
page wrote itself with file proves nothing about a command. A file block is
only a fixture: whatever its language, it is never run and never compared.
A text block directly after a bash block is that block's exact output. A
stream the page does not show must be empty: a command that prints a warning the
page omits fails the replay.
The replay fails closed. Each of these is a failure, not something it skips:
- an annotation it does not know, one written without its colon, one given
twice, one not directly before a block, two on one block, or
norunorexiton a block with nowesleycommand; - a fence left open, or a
wesleyline that would not run: indented, behind a wrapper such asenvorFOO=1, or in ansh,shellorconsolefence, since onlybashfences are replayed; - in a command that is run: a pipe, redirect, quote or other shell syntax, since
no shell interprets it, and an argument that is an absolute path or climbs
out with
..; - an output block with no command before it, a comparison that compares nothing, and a JSON block that repeats a key, since only the last would be compared;
- a
fileorshowspath outside the scratch directory; - a command that does not finish within ten seconds (
--timeout-mschanges the wait).
test/docs-replay-refusals.bats gives it one broken page per rule and requires
each to be refused with a message that names the problem.
The scratch directory is a working directory, not a sandbox. Refusing path arguments that leave it stops a documented command from writing into the checkout; it does not contain a command that finds a path some other way.
Prettier does not reformat code inside Markdown here, because an excerpt has to stay byte for byte what the tool printed.
scripts/install-hooks.sh points Git at .githooks/. The pre-commit hook keeps
the lockfile in step with manifest changes.
The pre-push hook chooses what to run from the paths a push changes
(scripts/pre-push-sanity.mjs). Rust, workflow, hook, and documentation paths
select cargo xtask preflight. packages/, the lockfile, and the package
manifests select the legacy preflight. .github/, .githooks/, scripts/,
test/, docs/, and README.md select the bats suites. A push that touches none of a group's paths
skips that group, so the hook is a shortcut, not the gate: CI runs everything.
Rust stable; the repository is developed on 1.96. Node ^22.13, ^24, or
>=26, with pnpm at the version package.json names in packageManager.
Corepack provides it: corepack enable.
actionlint is optional locally (brew install actionlint, or
go install github.com/rhysd/actionlint/cmd/actionlint@latest). Without it,
legacy-preflight says the workflows were not linted and carries on. In CI
(CI=true) a missing actionlint fails the run, so a workflow is never merged
unlinted.
The gate runs actionlint -shellcheck= -pyflakes=. Left alone, actionlint runs
whatever shellcheck and pyflakes are on the machine, so the same workflow
could pass on a laptop and fail on a runner, or start failing when the runner
image upgrades. With them off the verdict is the same everywhere. The cost is
that the shell inside run: blocks is not linted by this gate.