Skip to content

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.json names the backend where hushrun run shares results.
  • hushrun.yaml configures promotion: the repository's URL on hushrun, its scopes, checks, artifacts and stages, and the log key readers pin. hushrun run reads only its scopes, digest.exclude and repository.
  • 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

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"
  }
}
KeyWhat it is
hushrun.backendThe organization's URL where hushrun run records and looks up results. hushrun reads it from the package.json 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, when package.json names 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 run takes the first of HUSHRUN_BACKEND, this key, the backend hushrun login saved, and this machine's cache directory. Without a URL, hushrun login signs in to it unless HUSHRUN_BACKEND names another, and the hushrun token commands manage its tokens unless --backend or HUSHRUN_BACKEND does.
  • What it must be: an https:// URL, or http:// on localhost, 127.0.0.1 or [::1]. Anything else turns results off, with a line that names the file. A package.json that doesn't parse names no backend.
  • Which credential goes there: the token hushrun login saved for that server, and in GitHub Actions the job's ID token. A token in HUSHRUN_TOKEN doesn'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
# 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 full

Top level

KeyRequiredWhat it is
versionYes1
repositoryYesThe repository's canonical id, host/owner/name: a lowercase host, no scheme, no .git, such as github.com/acme/shop
repository_urlNoThe repository's URL on hushrun, where its events go, such as https://api.hushrun.com/l/acme/shop: https://, or http:// on localhost. In CI, HUSHRUN_REPOSITORY_URL must agree with it: see In CI
transparency_logNoThe server's log key: see transparency_log
scopesNoNamed sets of files, each with its own digest. Without it, one scope, default, covers every file
digestNoexclude: patterns left out of every scope
checksNoWhat can be checked, by name
artifactsNoWhat stages deploy, by name
stagesNoWhere 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

KeyDefaultWhat it is
scopes.<name>.include["**"]Patterns of the files in the scope. It can't be empty
scopes.<name>.exclude[]Patterns left out of the scope
digest.exclude[]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:

PatternMatches
** as a whole segmentAny number of whole segments. Last in a pattern, one or more: a/** matches a/b and a/b/c, not a file named a
*Any run of characters within one segment, possibly none
?One character within one segment
[abc], [a-z]One character of the class, never /
[!abc]One character not in the class, never /
Anything elseItself, \ 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

KeyDefaultWhat it is
kindRequiredsource: 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
runNoneThe command hushrun run --stage runs. Without it, another tool attests the check, or hushrun run --stage <stage> <check> -- <command> gives the command. An approval check has none
shellsh, or pwsh on Windows outside an isolated runWhat 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_TOKEN, doesn't run isolated
imageNoneThe image the check runs in, pinned by digest, …@sha256:…. Evidence counts only from the same image, and --isolated needs it
platformNoneThe platform, such as linux/amd64. Evidence counts only from the same one
networknoneThe network of an isolated run: none; restricted, the network HUSHRUN_RESTRICTED_NETWORK names; or open, the container runtime's default
timeoutNoneHow long hushrun run --stage lets the check run
retries0How many times hushrun run --stage runs a failed check again
max_ageNoneHow long evidence stays valid, such as 7d. Older evidence has expired
versionNoneFree 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

KeyDefaultWhat it is
typeRequiredoci, an image or index in a registry, or file
scopedefaultThe scope the artifact is built from
locations.<stage>Required for every stageWhere the stage's copy lives. For oci, a repository with its registry host and no tag or digest, such as us-docker.pkg.dev/acme/prod/api

Every stage deploys every artifact of its scope, so each artifact needs a location for every stage.

stages

KeyDefaultWhat it is
branchNoneThe 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
afterNoneThe upstream stage. Cycles are errors
require[]<check> for a source or approval check, and <check>@<stage> for a deployment check of an upstream stage
upstreamnot-failednot-failed: the upstream mustn't have failed this digest. passed: it must have passed it
upstream_grace30sHow long a plan that a push started waits for an upstream run to appear before deciding there is none
lease3hThe lease hushrun plan --start 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_forcetrueWhether hushrun plan --force-full may force this stage. With false, such a plan is blocked
on_repository_errorfailWhen 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.github.workflowNoneThe workflow file of the stage's runs, such as deploy-staging.yml, so that a later stage sees them queued before they record anything
ci.gitlab.refThe stage's branchThe 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

KeyDefaultWhat it is
originRequiredThe name the log's checkpoints carry: the repository's URL without its scheme, such as api.hushrun.com/l/acme/shop. Printable ASCII without spaces or +
keyRequiredThe 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 monitor refuses to run.
  • npx hushrun init writes origin and key when the server tells it, and hushrun doctor prints 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 as unit-test and unit_test, since outputs such as HUSHRUN_DIGEST_<SCOPE> use that form.
  • A duration is numbers with units, largest first: ms, s, m, h, d and w, such as 30s, 20m, 3h, 7d or 1h30m.

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 true or false, such as yes;
  • 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

DirectoryLinuxmacOSWindows
<config dir>~/.config~/Library/Application Support%APPDATA%
<cache dir>~/.cache~/Library/Caches%LOCALAPPDATA%

On Linux, XDG_CONFIG_HOME and XDG_CACHE_HOME replace ~/.config and ~/.cache.

FileWhereMoved byWhat it holds
The credentials file<config dir>/hushrun/credentials.jsonHUSHRUN_CREDENTIALS, the file's pathFor 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 keymachine-key.pem, beside the credentials fileHUSHRUN_MACHINE_KEY_FILE, the file's path; or HUSHRUN_MACHINE_KEY, the key itselfAn 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/HUSHRUN_CACHE_DIR; set to nothing, there is noneresults/, 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:

credentials.json
{
  "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.yaml pins, so the server can't change it unseen. Without a pinned key, hushrun takes the answer unchecked, and warns.
  • How hushrun signs, from HUSHRUN_SIGNING and 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:

  1. --repository-url;
  2. HUSHRUN_REPOSITORY_URL;
  3. HUSHRUN_REPOSITORY_BASE with hushrun.yaml's repository: <base>/<repository>;
  4. hushrun.yaml's repository_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:

  1. HUSHRUN_REPOSITORY_URL, set in CI settings that live outside the repository, such as GitHub repository variables or GitLab project variables;
  2. else HUSHRUN_REPOSITORY_BASE: <base>/<repository id>, with the id from the CI provider, not from the files. hushrun.yaml's repository must be exactly that id, and its repository_url that URL;
  3. else hushrun.yaml's repository_url, in the hushrun.yaml being evaluated.
  • Only HUSHRUN_REPOSITORY_URL counts: a --repository-url that differs exits 2.
  • When HUSHRUN_REPOSITORY_URL differs from hushrun.yaml's repository_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_URL and HUSHRUN_REPOSITORY_BASE are set, the URL wins, with a warning when the base gives another.
  • HUSHRUN_REPOSITORY_TOKEN and HUSHRUN_REPOSITORY_TOKEN_COMMAND go only to a URL that HUSHRUN_REPOSITORY_URL names or HUSHRUN_REPOSITORY_BASE gives. A URL only hushrun.yaml names gets the GitHub Actions job's ID token, whose audience names that URL; else, on hushrun's hosted server, an agent token in HUSHRUN_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 doctor reports the setting when its token can read it.
  • Commands that never touch the server, such as hushrun digest, hushrun keys and hushrun 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.

Markdown