Configuration
Where hushrun reads its configuration: package.json's hushrun key, hushrun.yaml, the files it keeps on each machine, and the environment. hushrun run needs none of it to skip checks.
package.jsonnames the backend wherehushrun runshares results.hushrun.yamlconfigures promotion: the repository's URL on hushrun, its scopes, checks, artifacts and stages, and the log key readers pin.hushrun runreads only its scopes,digest.excludeandrepository.- Environment variables and flags come first, with one exception in CI: see Where configuration comes from, Environment variables and the global options.
- Files hushrun keeps on each machine hold your sign-in, this machine's key and the cache.
package.json
{
"scripts": {
"lint": "hushrun run -- eslint .",
"test": "hushrun run -- vitest run"
},
"devDependencies": {
"hushrun": "^0.1.0"
},
"hushrun": {
"backend": "https://api.hushrun.com/o/acme"
}
}| Key | What it is |
|---|---|
hushrun. | The organization's URL where hushrun run records and looks up results. hushrun reads it from the package. at the repository's root, the git top level, and reads nothing else under hushrun |
- Who writes it:
npx hushrun init, with your organization's URL, whenpackage.jsonnames no backend. Committed with the code, it tells CI, your teammates and their agents where results go, with no variable to set. - How it ranks:
hushrun runtakes the first ofHUSHRUN_BACKEND, this key, the backendhushrun loginsaved, and this machine's cache directory. Without a URL,hushrun loginsigns in to it unlessHUSHRUN_BACKENDnames another, and thehushrun tokencommands manage its tokens unless--backendorHUSHRUN_BACKENDdoes. - What it must be: an
https://URL, orhttp://onlocalhost,127.0.0.1or[::1]. Anything else turns results off, with a line that names the file. Apackage.jsonthat doesn't parse names no backend. - Which credential goes there: the token
hushrun loginsaved for that server, and in GitHub Actions the job's ID token. A token inHUSHRUN_TOKENdoesn't follow it, so that a change to the file can't send a CI secret elsewhere, except an agent token,hra_…, when the backend is on hushrun's hosted server. See Which credential goes where.
A backend that isn't https:// turns results off, and the check runs:
hushrun: lint running without skipping: results are off (package.json's hushrun.backend: "http://example.com/o/acme" is not a repository URL: http:// is allowed only on a loopback host (localhost, 127.0.0.1, [::1]); use https://)hushrun.yaml
hushrun.yaml lives at the repository's root. hushrun reads the one in the content it evaluates: the commit by default, HEAD or --rev, and the working tree with --worktree. hushrun run without a stage and hushrun policy validate read the working tree's. --config (or HUSHRUN_CONFIG) names another file, and --config-ref reads it from another git ref, as a pull request's pipeline must (Pull requests).
npx hushrun init writes a first one, with version, repository, repository_url and transparency_log. hushrun init --pipeline and hushrun adopt write starters with stages. hushrun policy validate checks it.
A complete example
Every key hushrun accepts, with example values. The keys and digests written … come from elsewhere: npx hushrun init writes transparency_log's, and the image's digest is the one your CI runs.
# hushrun.yaml, at the repository's root. Unknown keys are errors.
version: 1 # required, and 1
repository: github.com/acme/shop # required: host/owner/name
repository_url: https://api.hushrun.com/l/acme/shop # the repository's URL on hushrun
transparency_log: # the server's log key, which readers pin
origin: api.hushrun.com/l/acme/shop # repository_url without its scheme
key: "ed25519:…" # npx hushrun init writes it
scopes: # without it, one scope, default, covers every file
api:
include: ["services/api/**", "go.mod", "go.sum", ".github/workflows/deploy-*.yml"]
exclude: ["**/*_test.go"]
web:
include: ["web/**", ".github/workflows/deploy-*.yml"]
digest:
exclude: ["**/*.md"] # left out of every scope
checks:
unit:
kind: source # source, deployment or approval
run: go test ./... # what hushrun run --stage runs
shell: bash # sh, bash, pwsh or cmd
env: [GOFLAGS] # variables the check depends on
image: golang@sha256:… # the image it runs in, pinned by digest
platform: linux/amd64
network: none # none, restricted or open, for --isolated
timeout: 20m
retries: 1
max_age: 7d # older evidence has expired
version: "2" # change it to invalidate earlier evidence
e2e:
kind: deployment # required as e2e@<stage>
run: ./scripts/e2e.sh
release:
kind: approval # a person approves it: no run
artifacts:
api-image:
type: oci # oci or file
scope: api # default: default
locations: # one for every stage
staging: us-docker.pkg.dev/acme/staging/api
production: us-docker.pkg.dev/acme/prod/api
stages:
staging:
branch: develop
require: [unit]
ci:
github: { workflow: deploy-staging.yml } # lets production see staging's queued runs
# gitlab: { ref: develop } # on GitLab: the ref whose pipelines are its runs
production:
branch: main
after: staging
require: [unit, e2e@staging, release]
upstream: passed # default: not-failed
upstream_grace: 30s
lease: 3h
dedupe: [push, upstream]
allow_force: true
on_repository_error: fail # fail or fullTop level
| Key | Required | What it is |
|---|---|---|
version | Yes | 1 |
repository | Yes | The repository's canonical id, host/: a lowercase host, no scheme, no ., such as github. |
repository_ | No | The repository's URL on hushrun, where its events go, such as https://: https://, or http:// on localhost. In CI, HUSHRUN_ must agree with it: see In CI |
transparency_ | No | The server's log key: see transparency_ |
scopes | No | Named sets of files, each with its own digest. Without it, one scope, default, covers every file |
digest | No | exclude: patterns left out of every scope |
checks | No | What can be checked, by name |
artifacts | No | What stages deploy, by name |
stages | No | Where it deploys, and what each stage requires |
A hushrun.yaml without stages is valid: it names the repository, its URL and the log key, and hushrun plan --stage exits 2 until the stage exists.
scopes and digest
| Key | Default | What it is |
|---|---|---|
scopes. | ["**"] | Patterns of the files in the scope. It can't be empty |
scopes. | [] | Patterns left out of the scope |
digest. | [] | Patterns left out of every scope |
A file belongs to a scope when it matches one of the scope's include patterns, none of its exclude patterns, and no digest.exclude pattern. A scope must cover everything that decides its artifact and its deployment: build files, the Dockerfile, the deploy workflow. A scope that matches no file fails when hushrun digests it. Once there are several scopes, hushrun run needs --scope: see Several projects in one repository.
Patterns match the whole path, /-separated and case-sensitively, the same on every OS:
| Pattern | Matches |
|---|---|
** as a whole segment | Any number of whole segments. Last in a pattern, one or more: a/ matches a/ and a/, not a file named a |
* | Any run of characters within one segment, possibly none |
? | One character within one segment |
[abc], [a- | One character of the class, never / |
[!abc] | One character not in the class, never / |
| Anything else | Itself, \ included, which never escapes |
services/api/** matches services/api/main.go, not services/apix/a.go; **/*.md matches README.md and docs/a/b.md; *.go matches main.go, not cmd/main.go. Braces, ** with other characters in its segment, empty segments, and a class that starts with ^ are errors.
checks
| Key | Default | What it is |
|---|---|---|
kind | Required | source: depends only on the files, and its evidence counts at every stage. deployment: belongs to one stage's environment, and is required as <check>@<stage>. approval: a person's sign-off, which hushrun approve records |
run | None | The command hushrun run - runs. Without it, another tool attests the check, or hushrun run - gives the command. An approval check has none |
shell | sh, or pwsh on Windows outside an isolated run | What runs run: sh, bash, pwsh or cmd |
env | [] | Names of the variables the check depends on. Their values are never recorded. An isolated run passes in only these, and a check that names a secret, such as NPM_, doesn't run isolated |
image | None | The image the check runs in, pinned by digest, …@sha256:…. Evidence counts only from the same image, and - needs it |
platform | None | The platform, such as linux/. Evidence counts only from the same one |
network | none | The network of an isolated run: none; restricted, the network HUSHRUN_ names; or open, the container runtime's default |
timeout | None | How long hushrun run - lets the check run |
retries | 0 | How many times hushrun run - runs a failed check again |
max_ | None | How long evidence stays valid, such as 7d. Older evidence has expired |
version | None | Free text. Change it to invalidate the check's evidence |
A change to kind, run, shell, image, platform, env, network or version gives the check a new definition, and its earlier evidence stops counting. timeout, retries and max_age aren't part of it.
artifacts
| Key | Default | What it is |
|---|---|---|
type | Required | oci, an image or index in a registry, or file |
scope | default | The scope the artifact is built from |
locations. | Required for every stage | Where the stage's copy lives. For oci, a repository with its registry host and no tag or digest, such as us- |
Every stage deploys every artifact of its scope, so each artifact needs a location for every stage.
stages
| Key | Default | What it is |
|---|---|---|
branch | None | The only branch the stage deploys from, a branch name such as main, never refs/. A plan for a run on another branch is blocked, and hushrun guard checks the branch's head |
after | None | The upstream stage. Cycles are errors |
require | [] | <check> for a source or approval check, and <check>@<stage> for a deployment check of an upstream stage |
upstream | not- | not-: the upstream mustn't have failed this digest. passed: it must have passed it |
upstream_ | 30s | How long a plan that a push started waits for an upstream run to appear before deciding there is none |
lease | 3h | The lease hushrun plan - records, and the longest any lease at this stage can be |
dedupe | [push, upstream] | The triggers, of push, upstream, manual and schedule, whose first attempt skips a digest the stage already deployed |
allow_ | true | Whether hushrun plan - may force this stage. With false, such a plan is blocked |
on_ | fail | When a plan can't read the repository's events: fail exits 6, full decides full. full isn't allowed on a stage with after, a deployment check or an approval check |
ci. | None | The workflow file of the stage's runs, such as deploy-, so that a later stage sees them queued before they record anything |
ci. | The stage's branch | The branch or tag whose pipelines are the stage's runs, when they aren't its branch's |
Whether a stage counts only CI's evidence isn't set in hushrun.yaml, where a pull request could remove it: an organization admin sets it on hushrun's server, and hushrun policy explain --stage <stage> shows it. See Promotion.
transparency_log
| Key | Default | What it is |
|---|---|---|
origin | Required | The name the log's checkpoints carry: the repository's URL without its scheme, such as api.. Printable ASCII without spaces or + |
key | Required | The public half of the server's log key, ed25519:<base64> |
- With the key, hushrun checks every receipt, listing and checkpoint the server gives against it. Without it, hushrun checks nothing and warns, and
hushrun repository monitorrefuses to run. npx hushrun initwritesoriginandkeywhen the server tells it, andhushrun doctorprints the lines otherwise.
See Transparency log.
Names and durations
- Scopes, checks, artifacts and stages are named with a lowercase letter, then lowercase letters, digits,
_and-, 63 characters at most. Two names of one kind can't become the same once uppercased with-as_, such asunit-testandunit_test, since outputs such asHUSHRUN_DIGEST_<SCOPE>use that form. - A duration is numbers with units, largest first:
ms,s,m,h,dandw, such as30s,20m,3h,7dor1h30m.
Validation
Besides the rules in the tables above, hushrun refuses a hushrun.yaml:
- with a key it doesn't know, at any level, or a key written twice;
- that refers to a check, stage or scope it doesn't define;
- with a boolean other than
trueorfalse, such asyes; - larger than 1 MiB.
Each error names the file, line and column, and the command exits 2:
$ hushrun policy validate error: hushrun.yaml:13:5: unknown field `accept`, expected one of branch, after, require, upstream, upstream_grace, lease, dedupe, allow_force, on_repository_error, ci
Where hushrun keeps its files
| Directory | Linux | macOS | Windows |
|---|---|---|---|
<config dir> | ~/ | ~/ | %APPDATA% |
<cache dir> | ~/ | ~/ | %LOCALAPPDATA% |
On Linux, XDG_CONFIG_HOME and XDG_CACHE_HOME replace ~/.config and ~/.cache.
| File | Where | Moved by | What it holds |
|---|---|---|---|
| The credentials file | <config dir>/ | HUSHRUN_, the file's path | For each server you signed in to, your token, your email and this machine's key id; and the backend hushrun run uses when nothing else names one. Readable by you only, mode 0600 on Unix |
| This machine's key | machine-, beside the credentials file | HUSHRUN_, the file's path; or HUSHRUN_, the key itself | An ECDSA P-256 key, made the first time the machine signs in or records with a personal or agent token, mode 0600. hushrun refuses one inside the repository it runs in |
| The cache | <cache dir>/ | HUSHRUN_; set to nothing, there is none | results/, hushrun run's results when no backend applies; file hashes; the tokens this machine registered with; the Sigstore trust root hushrun sigstore refresh keeps; and exports/, for isolated runs |
HUSHRUN_CREDENTIALS moves the machine key with it, unless HUSHRUN_MACHINE_KEY_FILE is set. The credentials file looks like this:
{
"backend": "https://api.hushrun.com/o/acme",
"hosts": {
"https://api.hushrun.com": {
"token": "…",
"email": "ana@acme.dev",
"machine": "sha256:…"
}
}
}Where configuration comes from
A flag wins over its environment variable, a variable over hushrun.yaml, and hushrun.yaml over the defaults. In CI, the repository's URL is the exception. Two things never come from the repository's files:
- Who counts as a signer. hushrun's server answers it, signed with the log key
hushrun.yamlpins, so the server can't change it unseen. Without a pinned key, hushrun takes the answer unchecked, and warns. - How hushrun signs, from
HUSHRUN_SIGNINGand the variables beside it: see Signing.
Where results go
hushrun run takes the first of HUSHRUN_BACKEND, package.json's hushrun.backend, the backend hushrun login saved, and this machine's cache directory, in CI as anywhere else. What differs is the credential: see Where results go.
Locally
Outside CI, the repository's URL is the first of:
--repository-url;HUSHRUN_REPOSITORY_URL;HUSHRUN_REPOSITORY_BASEwithhushrun.yaml'srepository:<base>/<repository>;hushrun.yaml'srepository_url.
hushrun.yaml is the evaluated revision's, as committed. Without any of these, the command exits 2. When only the working tree's hushrun.yaml names a repository_url, as right after npx hushrun init, the message says to commit it or pass --config hushrun.yaml.
HUSHRUN_REPOSITORY_TOKEN and HUSHRUN_REPOSITORY_TOKEN_COMMAND go only to a URL that --repository-url or HUSHRUN_REPOSITORY_URL names. A URL on the server you signed in to gets the token hushrun login saved there, and one on hushrun's hosted server gets an agent token in HUSHRUN_TOKEN, even when only hushrun.yaml names it.
In CI
In CI, any provider hushrun detects (see CI detection), the commands that read or write the repository's events take its URL from:
HUSHRUN_REPOSITORY_URL, set in CI settings that live outside the repository, such as GitHub repository variables or GitLab project variables;- else
HUSHRUN_REPOSITORY_BASE:<base>/<repository id>, with the id from the CI provider, not from the files.hushrun.yaml'srepositorymust be exactly that id, and itsrepository_urlthat URL; - else
hushrun.yaml'srepository_url, in thehushrun.yamlbeing evaluated.
- Only
HUSHRUN_REPOSITORY_URLcounts: a--repository-urlthat differs exits 2. - When
HUSHRUN_REPOSITORY_URLdiffers fromhushrun.yaml'srepository_url, compared with the scheme and host in lowercase and no trailing/, the command exits 2. So does a CI where none of the three is set. - When both
HUSHRUN_REPOSITORY_URLandHUSHRUN_REPOSITORY_BASEare set, the URL wins, with a warning when the base gives another. HUSHRUN_REPOSITORY_TOKENandHUSHRUN_REPOSITORY_TOKEN_COMMANDgo only to a URL thatHUSHRUN_REPOSITORY_URLnames orHUSHRUN_REPOSITORY_BASEgives. A URL onlyhushrun.yamlnames gets the GitHub Actions job's ID token, whose audience names that URL; else, on hushrun's hosted server, an agent token inHUSHRUN_TOKEN, which that server takes for its org only. That is how a GitLab pipeline writes (GitLab CI). A person's login never goes to the repository's server in CI.- When the provider reports the repository's numeric id, a server whose answer names another repository's CI makes the command exit 2.
- On GitLab, pipeline variables override project variables, so limit who may set them;
hushrun doctorreports the setting when its token can read it. - Commands that never touch the server, such as
hushrun digest,hushrun keysandhushrun policy validate, need no URL.
Pull requests
hushrun.yaml is read from the revision under evaluation, because it is reviewed with the code it governs. A pull request's own hushrun.yaml hasn't been reviewed yet, so in a pull request's pipeline, GitHub's pull_request and pull_request_target events and GitLab's merge request pipelines, the commands that read or write the repository's events exit 2 unless --config-ref names the branch it merges into, as main or origin/main:
git fetch --depth=1 origin "$GITHUB_BASE_REF"
hushrun plan --stage pr --config-ref "origin/$GITHUB_BASE_REF"--config-ref reads hushrun.yaml as that ref has it, for the scopes, checks and stages alike. A ref without the file exits 2. Commands that touch no server, such as hushrun policy validate, read the pull request's own file.