# 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](#sources), [Environment variables](https://hushrun.com/docs/reference/environment.md) and the [global options](https://hushrun.com/docs/reference/cli.md#global-options).
- **Files hushrun keeps** on each machine hold your sign-in, this machine's key and the cache.

## package.json

```json title="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.backend` | The 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](https://hushrun.com/docs/results.md#credentials).

A backend that isn't `https://` turns results off, and the check runs:

```text
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](#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.

```yaml title="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

| Key | Required | What it is |
|---|---|---|
| `version` | Yes | `1` |
| `repository` | Yes | The repository's canonical id, `host/owner/name`: a lowercase host, no scheme, no `.git`, such as `github.com/acme/shop` |
| `repository_url` | No | The 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](#in-ci) |
| `transparency_log` | No | The server's log key: see [`transparency_log`](#transparency_log) |
| `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.<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](https://hushrun.com/docs/run.md#monorepos).

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/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 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 --stage` runs. Without it, another tool attests the check, or `hushrun run --stage <stage> <check> -- <command>` 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_TOKEN`, doesn't run isolated |
| `image` | None | The image the check runs in, pinned by digest, `…@sha256:…`. Evidence counts only from the same image, and `--isolated` needs it |
| `platform` | None | The platform, such as `linux/amd64`. Evidence counts only from the same one |
| `network` | `none` | The network of an isolated run: `none`; `restricted`, the network `HUSHRUN_RESTRICTED_NETWORK` names; or `open`, the container runtime's default |
| `timeout` | None | How long `hushrun run --stage` lets the check run |
| `retries` | `0` | How many times `hushrun run --stage` runs a failed check again |
| `max_age` | 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.<stage>` | 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-docker.pkg.dev/acme/prod/api` |

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-failed` | `not-failed`: the upstream mustn't have failed this digest. `passed`: it must have passed it |
| `upstream_grace` | `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 --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_force` | `true` | Whether `hushrun plan --force-full` may force this stage. With `false`, such a plan is blocked |
| `on_repository_error` | `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.github.workflow` | None | The 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.ref` | 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](https://hushrun.com/docs/promotion.md).

### `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.hushrun.com/l/acme/shop`. 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 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](https://hushrun.com/docs/promotion/transparency.md).

### 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:

```console
$ 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>` | `~/.config` | `~/Library/Application Support` | `%APPDATA%` |
| `<cache dir>` | `~/.cache` | `~/Library/Caches` | `%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/credentials.json` | `HUSHRUN_CREDENTIALS`, 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-key.pem`, beside the credentials file | `HUSHRUN_MACHINE_KEY_FILE`, the file's path; or `HUSHRUN_MACHINE_KEY`, 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/` | `HUSHRUN_CACHE_DIR`; 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:

```json title="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](https://hushrun.com/docs/reference/environment.md#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](https://hushrun.com/docs/results.md).

### 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](https://hushrun.com/docs/reference/environment.md#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](https://hushrun.com/docs/promotion/gitlab.md#signing)). 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`:

```bash
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.
