# Commands

https://shipcheck.run/docs/cli/

> The commands, their exit codes and their environment variables.

| Command | Does |
|---|---|
| `shipcheck run [--sha <rev> \| --staged] [--no-fetch]` | Runs the checks in `shipcheck.toml`, host checks or workflows, and waits for the verdict. With no `--sha`, it tests the working copy: tracked edits and untracked files that are not gitignored, which it lists. `--staged` tests the index. A working copy with no change is `HEAD`. `--sha` tests that commit only. Fetches the remote's default branch first unless `--no-fetch`. A tree that already passed answers at once. A change outside `paths` runs nothing and prints `nothing ran`, and that skip is never stored. For each failed job it prints the failed step and its exit code, the failing tests by name, the last 60 lines of the step and the path of the full log. A failed host check gets its tests and the last 60 lines of its output. `--json` prints one object instead, see [JSON](#json) |
| `shipcheck run --job <id> \| --workflow <file>` | A partial run, for the loop after a failure: runs only those jobs, or every job of those workflows, and the jobs they `needs`. Both flags repeat. A matrix job's id runs all its legs. `--job <file>/<job>` picks the job of one workflow. In host mode `--job` names a `[host]` check and runs only it, and `--workflow` is an error. When a host check and an act job share a name, a bare name picks the host check. The result is never stored, so it is never the check: `status` ignores it, and `shipcheck commit` refuses after it, even with `--allow-failed`. A `--job` that names no job lists each one it takes, with its file and matrix legs, such as `test (ci.yml/test, legs test__1, test__2)`. Exit codes as usual. Fix, run the failed job as often as needed, then one full `shipcheck run` and `shipcheck commit` |
| `shipcheck commit -m <message> \| -F <file> [--allow-failed] [--no-verify]` | Commits exactly the tree the last `shipcheck run` of this working copy tested, through `git commit`, so hooks and signing work as usual. Refuses when a file changed since the run, when the last run was partial, and when the run failed unless `--allow-failed`. `--no-verify` goes to `git commit`. The commit's check is then that run's result. Refuses with exit 2 on the default branch, since PRs carry the checks: `git switch -c <branch>` first. Plain `git commit` still works there |
| `shipcheck doctor` | Checks git, act, the Docker engine and its versions, or in host mode the OS sandbox, copy-on-write, the count and size of shipcheck's Docker volumes and images, the remote's default branch and its `shipcheck.toml`, the network setting, colima when a job uses it, the hooks, whether `shipcheck post` can post, and where shipcheck's secrets are, reading each so that after an upgrade the keychain asks here. Marks each line `ok` or `fix`, a green check or a red cross in a terminal, with the command that fixes it. Exits 0 when a run can work, 1 otherwise |
| `shipcheck status [<rev> \| --staged] [--json]` | Prints the stored result for the tree `run` would test. With no revision it first prints whether the working copy's tree and the index's tree have a result, so you know before a commit whether it reuses one. With no result it exits 2, and with `--json` it prints `{"tree": "<id>", "result": null}`. Without `--base`, a tree that has no result under `main`'s `shipcheck.toml` shows its last run's, such as one with `--base <branch>`, and says which base it read |
| `shipcheck logs [<rev> \| --staged] [--base <ref>]` | Prints the full logs of that tree, one per check or workflow, then the last partial run's, with control characters escaped. Without `--base` it finds the last run of the tree as `status` does |
| `shipcheck history [--json] [--limit <n>]` | Prints the last runs of this repository on this machine, 20 by default: when, commit, mode, result, time, whether a stored pass answered, and what started the run. `--json` prints the records as one array. See [Run records](#run-records) |
| `shipcheck post [<rev>] [--home-ok]` | Posts the stored result of the commit's tree on GitHub as one completed check per job, `shipcheck/<job>`, and a `shipcheck` summary. A job id two workflows share gets the workflow in its name, as [require the checks](https://shipcheck.run/docs/branch-protection/#with-a-ruleset-recommended) says. The job checks are the ones to require. A job `main` has that the result lacks posts as a failure. Posts only a result read from the daemon's base for this clone or from the default branch the remote reports, so a trial with `--base` never posts. When the remote does not answer, only the daemon's base posts. Refuses when `main` has no `shipcheck.toml`, and when the commit is the tip of `main`, locally or on `origin`, since PRs carry the checks. Each job check carries its log tail as `logs` in `shipcheck.toml` says, every job by default and always a failed one, and anyone who can read the repository sees it. Each check shows how long its job took. Refuses when `SHIPCHECK_HOME`, `HOME` or `XDG_STATE_HOME` moves the results away from the default, since an agent could have written them there. Pass `--home-ok` if you set it yourself. The daemon takes `--home-ok` too |
| `shipcheck login` | Signs this machine in to shipcheck's service with GitHub, so `shipcheck post`, the pushes' runs and the daemon post the checks. Posting needs the shipcheck App installed on the repository and write, maintain or admin access for you. Prints a code to enter at github.com/login/device, and in a terminal Enter opens that page in the browser. Then it prints `signed in as <login>`. The machine token stops working after 90 days without a post, then run it again. On macOS it goes in the login keychain, see [where shipcheck keeps its keys](https://shipcheck.run/docs/install/#where-shipcheck-keeps-its-keys) |
| `shipcheck logout [--all]` | Revokes this machine's token and deletes it. With `SHIPCHECK_TOKEN` set, revokes that token instead and keeps the stored login token. `--all` revokes every token of your GitHub user, on every machine, and prints how many. When this machine's token is no longer valid, `--all` revokes nothing and fails, so sign in again first |
| `shipcheck token [--name <machine>]` | Makes a machine token for another machine to use as `SHIPCHECK_TOKEN`. Prints a code to enter at github.com/login/device on stderr, as `login` does, then only the token on stdout. Leaves this machine's sign-in alone. `--name` labels it, `token` by default. Each user can hold 50 live tokens |
| `shipcheck init [--yes]` | For the repository owner: turns each workflow job it can into a host check and each job act can run as GitHub does into a `[jobs]` act entry, tries each host check in the sandbox, writes `shipcheck.toml` with the ones that pass, writes the agent instructions into `AGENTS.md` and the other files agents read, and runs `install`. On a terminal its steps are tabs, Checks, Install, Workflows, GitHub and Review, which also cover where each job runs, the App, hosted Actions only on `main`, and a ruleset that protects the default branch. It changes nothing until you apply on the Review tab. `--yes` applies the recommended choices without asking, but opens no browser and creates no ruleset. Without a terminal and without `--yes` it only previews: it prints the plan `--yes` would apply and writes nothing, so an agent or a script passes `--yes`. It leaves an existing `shipcheck.toml` as it is. When the remote does not say its default branch, it refuses and says to run it again online. See [the quickstart](https://shipcheck.run/docs/quickstart.md) |
| `shipcheck install` | For each clone: installs the pre-push and post-commit hooks, removes the agent hooks an older version wrote, and starts the daemon service for this repository, with the default branch the remote reports as its base. When the remote does not answer, it adds no repository to the daemon and says to run it again online. Running it again changes nothing, and an error with the service is one line that never fails the install. In a terminal, it signs the machine in when it cannot post yet. Without one it prints how to. See [Agents](https://shipcheck.run/docs/agents.md) |
| `shipcheck daemon [--home-ok \| --uninstall]` | `init` and `install` start it as a background service, a launchd agent on macOS or a systemd user unit on Linux, and add the repository to `daemon.toml` in `SHIPCHECK_HOME`. It checks the open PRs into the configured branch within seconds of each push to a PR, from the App's webhook through shipcheck's service, and every `interval_seconds` (10 by default) while it cannot hear those events or a head is still waiting. A head with no completed `shipcheck` check gets its stored result at once, or, with no result, a run that posts in progress and then the result, as a push does. It runs only PRs you, or a login `daemon_authors` lists, both opened and pushed the head of, never a head that contains one it refused, and never a fork's. It posts with the `shipcheck login` token, and runs no PR until the machine is signed in. `--uninstall` stops and removes the service, but only from the default `SHIPCHECK_HOME`, since the service belongs to it |
| `shipcheck trust [--remove]` | Lets this repository's host checks with `sandbox = false` on `main` run outside the sandbox on this machine, with your full access. Without it they run in the sandbox. `--remove` takes it back. It records the origin URL in `trusted` in `SHIPCHECK_HOME` ([outside the sandbox](https://shipcheck.run/docs/config/#outside-the-sandbox)) |
| `shipcheck clean [--all]` | Removes what shipcheck keeps for this repository: its Docker cache volumes, warm checkouts and run slots, host mode's tool caches in `hostcache/` in `SHIPCHECK_HOME`, and the colima VMs this home recorded and a dead run left. `--all` does it for every repository, also deletes the colima base VM, removes stored results, logs and run records, and removes the daemon service when `SHIPCHECK_HOME` is the default. It never removes a Docker image. A volume that a container uses, running or stopped, stays, and so does a tool cache a run holds. Everything rebuilds on the next run, which is slower once. Refuses while a run is going |

The git hooks call `shipcheck on-push` and `shipcheck on-commit`. Those start a background run when `on` asks for one, never fail, and never make git wait. On a machine that can post, a push's run posts the checks. They read `shipcheck.toml` from the base `install` stored for the clone, else `origin/main`, and never ask the remote, so git does not wait on the network. With no stored base on a repository whose default branch is not `main`, they print one line to stderr that says to run `shipcheck install`. You do not call them yourself.

`--base <ref>` reads `shipcheck.toml` and the workflows from another ref than the default branch, and turns off the fetch. A result from another ref is a trial, which `post` refuses. It is for trying shipcheck on a repository before the file is on `main`.

## JSON

`--json` prints one object on stdout and the progress on stderr. Exit codes stay the same.

```json
{
  "tree": "4b825dc6...", "sha": "9f2c1e0a...", "partial": false, "passed": false, "could_not_run": false, "skipped": null, "ci_changes": [],
  "workflows": [{
    "file": "ci.yml", "passed": false, "exit": 1, "attempts": 1, "log": ".../logs/<key>/ci.yml.log",
    "jobs": [{
      "id": "test", "status": "failure", "attempts": 1, "shards": null,
      "failed_step": "Run pytest", "exit": 1, "note": null,
      "failing_tests": ["tests/test_math.py::test_add"], "failing_tests_total": 1,
      "tail": ["...", "FAILED tests/test_math.py::test_add - assert 3 == 4"],
      "log": ".../logs/<key>/ci.yml/test.log", "attempt_logs": []
    }]
  }]
}
```

Test names come from pytest, cargo test, nextest, go test, jest, vitest, rspec and phpunit output, at most 30. `sha` is `working copy` or `index` when the run tested one of those. `partial` is true for a `--job` or `--workflow` run. In host mode, one workflow named `host` holds a job per check, with no `failed_step` or `exit`, and `tail` is the end of the check's output. `failing_tests_total` counts them all. `tail` holds at most 60 lines, with control characters escaped. `note` explains a failure that is not the code's, such as a stale checkout or a job with no image on this host.

## Run records

Each run adds one line to `records/<owner>/<repo>.jsonl` in `SHIPCHECK_HOME`: when it ran, the commit and tree, the branch, what started it, the machine name and OS, the shipcheck and tool versions, whether a stored pass answered, and for each job its status, attempts, times and failing test names. The daemon also adds the PR number and author, whether it posted the checks and the comment, when a PR it checked gets a new head, and when one is merged and by whom. A record holds no code, file contents, log output, environment values or secrets.

`shipcheck history` reads them from this file.

Records are not deleted with old results and logs. A file past 50 MB moves to `<repo>.<n>.jsonl`. To delete them, remove the `records` folder, or run `shipcheck clean --all`, which also removes results, logs and warm checkouts.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Every check passed, or nothing under `paths` changed |
| 1 | A check failed, or `doctor` found something to fix. The output names the failed step and tests, `shipcheck logs` prints every log |
| 2 | shipcheck could not run, for example `shipcheck.toml` is missing on `main` or invalid, the OS sandbox is missing in host mode, or act is not installed in act mode. The message says what to fix. For `status`, the tree has no result yet |

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `SHIPCHECK_HOME` | `$XDG_STATE_HOME/shipcheck`, else `~/.local/state/shipcheck` | Where results, logs and warm checkouts live. Only `~/.local/state/shipcheck` counts as the default home: when `SHIPCHECK_HOME` or `XDG_STATE_HOME` moves it, `post` and the daemon refuse without `--home-ok`, and `init` and `install` leave the daemon service out. A home left at `~/Library/Application Support/shipcheck` by an older shipcheck moves here on first use. The default is inside a dot folder, so a test that refuses paths with a hidden folder in them fails there: set `SHIPCHECK_HOME` elsewhere, and pass `--home-ok` to post. Background runs log to `logs/background.log` in it. At the start of each run, results untouched for 14 days, logs untouched for 90 days and warm checkouts no run used for 7 days are deleted. At its end, the Docker volumes of this home that no run used for 14 days are removed, unless a container uses them, and so are the runner images an older shipcheck pinned. shipcheck never removes a volume it did not make, or an image a workflow names |
| `SHIPCHECK_TIMINGS` | unset | Set, `run` prints how long each phase of each job took to stderr, such as the clone, act's start and the cleanup |
| `SHIPCHECK_MAX_RUNS` | a quarter of the CPUs, at least 1 | How many runs the hooks and the daemon start at once on this machine. Others wait for a slot. `shipcheck run` never waits |
| `SHIPCHECK_API` | `https://api.shipcheck.run` | The service `login`, `logout`, `token` and `post` talk to |
| `SHIPCHECK_NO_DAEMON` | unset | `1` makes `init` and `install` leave the daemon service out on this machine |
| `SHIPCHECK_SKIP` | unset | `1` on a commit or push starts no background run, see [skips](https://shipcheck.run/docs/config.md) |
| `SHIPCHECK_TOKEN` | the token from `shipcheck login` | A machine token for `post`, for an unattended machine such as a VM or an agent box. Make one with `shipcheck token`. When set, `post` uses it instead of the login token, and `logout` revokes it. The daemon service gets only `PATH`, so it never sees it ([the daemon](https://shipcheck.run/docs/install/#the-daemon)) |
