How skipping works
hushrun run -- COMMAND runs COMMAND unless this exact code already passed it, and records the result where your laptop, CI and your agents find it.
The command
hushrun run [NAME] -- COMMANDEverything after -- is the check's command, run as it is. In package.json, it wraps a script once, and npm test then skips the same way everywhere:
"scripts": {
"lint": "hushrun run -- eslint .",
"test": "hushrun run -- vitest run"
}- hushrun exits with the command's exit code, or 0 when it skips.
- Its own lines go to stderr and start with
hushrun:.-qsilences them. Stdout is the command's. - It needs no configuration file, key or account. Without a backend, results stay on this machine.
What counts as the same code
hushrun skips when it finds a pass recorded for the same key. The key is made of:
- The repository's files as they are now: tracked and untracked, committed or not, with the files
.gitignoreignores left out. Change a file and the check runs again; go back to files that passed and it skips. - The check's name.
- The exact command, argument by argument.
vitest runandvitest run src/cart.test.tsare two checks, so a partial run never stands in for a full one. - The directory it runs in, relative to the repository's root.
Nothing else is part of it: not the machine, the operating system, the CI provider, the environment variables, the branch, the commit or the time. A pass on your laptop skips the check in CI, and the other way round. Committing changes nothing.
$ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test running: no pass yet for this exact code … hushrun: test passed in 0.6s; it won't run again on this code
A change to cart.js that breaks a test runs it again, and the failure keeps vitest's exit code:
$ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test running: no pass yet for this exact code … Test Files 1 failed (1) Tests 1 failed | 1 passed (2) … hushrun: test failed in 0.3s (exit 1)
Undo the change and the earlier pass counts again:
$ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test skipped: this exact code passed just now (saves 0.6s)
When a check depends on something outside the files, such as an environment variable, the Node.js version or the operating system, give each variant its own name, such as unit-node22 and unit-node24.
Naming checks
The name labels a check's history and is part of its key. Without NAME:
- run through a
package.jsonscript, the check takes the script's name:"test": "hushrun run -- vitest run"istest, with npm, pnpm, Yarn and Bun alike; - otherwise it takes the program's name:
hushrun run -- vitest runat a prompt isvitest.
A name starts with a lowercase letter, followed by lowercase letters, digits, _ and -, 63 characters at most, and hushrun makes one from a script's or a program's name: test:unit names its check test-unit, and format:check names it format-check. Name a check yourself when you want another name in its history:
"scripts": {
"test:unit": "hushrun run unit -- vitest run --project unit",
"test:e2e": "hushrun run e2e -- playwright test"
}What it prints
| Line | What happened |
|---|---|
test running: no pass yet for this exact code | No result for this key: the command runs |
test passed in 41s; it won't run again on this code | It passed and was recorded |
test skipped: this exact code passed 3 min ago (saves 41s) | A pass was found: the command didn't run, and hushrun exited 0 |
test skipped: this exact code passed in CI 3 min ago (saves 41s) | The same, for a pass CI recorded |
test running: it failed on this exact code last time | The newest result for this key is a fail: it runs again |
test failed in 2s (exit 1) | It failed; the failure was recorded and the exit code kept |
test running (- | - runs it even after a pass |
test running: can't read earlier results (…) | The backend couldn't be reached: it runs, and hushrun still tries to record |
test stopped by a signal; nothing recorded | Interrupted, for example with Ctrl-C |
Lines about credentials, tokens and plan limits are in Troubleshooting.
When hushrun runs anyway
Whenever hushrun can't tell whether this code passed, it runs the command. At worst a doubt costs a run; it never skips a check it shouldn't.
- Outside a git repository, it runs and records nothing.
- With results turned off (
HUSHRUN_BACKEND=set to nothing), it runs, records nothing and prints nothing. - When the backend can't be reached within 10 seconds, or answers something unexpected, it runs, and still tries to record.
- When the backend refuses the credential, it says so in one line, runs, and doesn't record.
- Without the sign-in a backend needs, it runs, and at a terminal ends with
hushrun: sign in to share results with acme: npx hushrun login. - When
hushrun.yamlhas several scopes and no--scopesays which one, it can't tell which files the check covers, and says so (see Several projects in one repository).
Files that change while it runs
hushrun checks the files again when the command ends. If they changed, the result belongs to neither version, so it isn't recorded:
hushrun: test passed in 12s, but files changed while it ran, so it wasn't recorded. If test writes files, list them in .gitignoreList what your checks write, such as coverage/, test-results/ or build output, in .gitignore.
Failures and flaky checks
A failure is recorded too, and the newest result for a key decides the next run, so a fail always runs again. A pass and a fail on the same key means the check is flaky: it gave two answers on the same code. The repository's page in the dashboard marks those checks.
Running a check again
--force runs a check after a pass, and records the new result:
npx hushrun run --force test -- vitest runTo run a package.json script's check again, give its name and its exact command, from the same directory, so the key is the same. npx hushrun finds the hushrun package that init added to devDependencies.
Several projects in one repository
By default a check's key covers every file in the repository, so a change anywhere runs every check again. hushrun.yaml at the repository's root narrows it in two ways.
Leave out files that never change a check's result:
version: 1
repository: github.com/acme/shop
digest:
exclude: ["**/*.md"]Split the repository into scopes, and give each check its scope:
version: 1
repository: github.com/acme/shop
scopes:
web:
include: ["web/**", "package.json", "pnpm-lock.yaml"]
api:
include: ["api/**", "go.mod", "go.sum"]"scripts": {
"test": "hushrun run --scope web -- vitest run"
}A change under api/ no longer runs web's checks again. Once hushrun.yaml has more than one scope, every hushrun run needs --scope; without it, the check runs every time, and hushrun says why:
hushrun: test running without skipping: can't tell which code this is (this policy has several scopes (api, web); pass --scope NAME or --all-scopes)Configuration has the whole of hushrun.yaml.
Next
- Where results go: your machine, your organization, or your own server.
- In CI and Agents and sandboxes.