Quickstart
Setup
- Install the binary:
brew install gabrielkoerich/tap/shipcheck, orcurl -fsSL https://shipcheck.run/install.sh | sh(install). - Run
shipcheck initin a clone of the repository, onmain. - Commit the
shipcheck.tomlit writes and merge it tomain. - Install the shipcheck App on the repository.
initprints this link.
Each other clone then needs shipcheck install once. The sections below say what each step does.
For the repository owner, once
In a clone of the repository, on main:
shipcheck init
It reads your workflows and project files and changes nothing until you apply. Its steps are tabs: Checks, Install, Workflows, GitHub and Review. Move between them with the arrow keys, Tab or 1 to 5, and go back to any of them. Each starts on the recommended choice. On Checks, Space or Enter moves a job to another place it can run: a host check, act on Docker, act on a colima VM where colima could run it, or GitHub. A job becomes a host check only when it ran as one in init's trial. Apply on the Review tab. Quitting, or Ctrl-C at any point before you apply, leaves the repository and the machine as they were.
An agent or a script runs shipcheck init --yes, which asks nothing and applies the recommended choices, except the browser and the ruleset. Without a terminal and without --yes, init only previews: it tries each check with a temporary SHIPCHECK_HOME, prints the plan and the review --yes would apply, and writes nothing.
Checks
init turns each job of a workflow that GitHub runs for feature branches into a check named like the job.
- The job's
runsteps are joined by&&. - A job with
needsruns the jobs it needs first. - Package installs into the checkout, such as
npm ci,bun install --frozen-lockfileoruv sync, stay in the check. - The workflow's and the job's
envare exported first, and a step'senvonly in that step. - Commands that set up the machine, such as
apt,sudo,curl,brew,rustup,pip install,go install,git configorgh, are left out. A step that needs root besides other commands, such as one that makes and mounts a volume, is left out whole, and so is aTMPDIRorHOMEvalue, since the sandbox sets both. - Steps with a GitHub expression, also in their
env,if:orcontinue-on-errorare left out, and so are multi-line steps with control flow. - A setup action that installs into the checkout keeps its install:
pnpm/setup(unlessinstall: false) andpnpm/action-setupwithrun_install: trueaddpnpm install,ruby/setup-rubywithbundler-cache: trueaddsbundle install.setup-nodeandsetup-uvcaches install nothing.gitleaks/gitleaks-actionbecomes agitleaks dir --redactscan of the commit's tracked files, which leaves out the build output a warm clone holds. A history scan stays on GitHub. - Deploy, publish and release jobs are left out, and so are jobs that only install, move files or run git, jobs that download artifacts from a job with no check, and jobs that need a Rust target (
rustup target add,targets). A pinned Rust toolchain, such astoolchain: "1.94.1"fordtolnay/rust-toolchainoractions-rust-lang/setup-rust-toolchain, is pinned withRUSTUP_TOOLCHAINwhen rustup has it here. Otherwise the check runs on the default toolchain, and init lists it under "run on this machine's version, not the workflow's" until you runrustup toolchain install 1.94.1. Arust-toolchain.tomlneeds nothing, since rustup reads it. - A job no host check runs as GitHub does, such as one with a dropped step, a
container:or a Linux matrix, runs with act as a[jobs]entry when Docker is running, and init addsnetwork = "internet". A job stays on GitHub when act cannot run it as GitHub does either: Windows jobs, macOS jobs off a Mac, matrices with macOS or Windows entries, and jobs that read the PR anywhere, report to GitHub, or run Docker. init writesengine = "colima"only when you move a job to a colima VM on the Checks tab. A matrix of one combination, such aspython-version: [3.13], is a plain job. Which jobs run where has the table.
Only when no job gives a check, a package.json test or lint script, a Cargo.toml, a pyproject.toml or a go.mod at the root gives the checks they imply. A repository with no .github/workflows gets its checks this way, in host mode, the only mode it can use.
It shows each check with the file it came from, and what it left out and why. The checks run natively in a sandbox, and before the review init runs each one there once, as shipcheck run would. A check that fails runs again without forced color, then with color off. A job whose check still fails stays on GitHub, and init prints the end of its log and the fix. init does not try the act jobs, so a failing one shows on its first run. Edit shipcheck.toml afterwards to drop one:
runner = "host"
[host]
versions = ["cargo --version", "rustc --version"]
[host.checks]
test = "cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test"
service = "cd service && bun run test"
shipcheck has two modes. init writes host mode when it finds checks, with act entries for the jobs a host check cannot run as GitHub does. It writes act mode for the whole repository when a job runs a Linux matrix, every job runs on Linux and Docker is running, since host mode tests only this machine's toolchain:
- Host mode runs your commands natively in an OS sandbox, each in a warm copy-on-write clone. It is fast, uses the tools on your machine and needs no Docker.
- act mode,
runner = "act", runs your GitHub Actions workflows in containers. It is the closest to GitHub's runners and slower to start. It needs act and a running Docker engine, and so does each[jobs]act entry in host mode (install). Ashipcheck.tomlwith norunnerline runs act mode.
GitHub: the App
It shows the install link of the shipcheck App, which posts the checks, and opens it in the browser when you apply. Install it on the repository.
Pick your user or organization and the repositories there (install the App). At the end, init signs this machine in if it cannot post yet (sign in each machine).
Workflows: hosted Actions only on main
For each workflow that GitHub runs for feature branches, it shows one line, such as ci.yml: on PRs shipcheck runs shipcheck/clippy, shipcheck/fmt, shipcheck/test, GitHub keeps running ci.yml on pushes to main, and on fork PRs, and a diff. The workflow keeps its triggers. Each job a shipcheck check covers gets an if: that runs it on GitHub only on your default branch, on the other branches the workflow lists for pushes, on PRs into other branches, for Dependabot, and for a fork's PR, which shipcheck never runs. A job no check covers stays on GitHub with no if: added (which jobs run where).
- Comments and other triggers stay.
- The recommended choice is yes. init compares the commands, not the names, so a check called
fmtcovers a job calledtestthat runscargo fmt --check. The line names the jobs GitHub keeps running on PRs.
GitHub: branch protection
It shows one check name per job, such as shipcheck/test, and GitHub's own check names of the jobs that stay on GitHub. The recommended choice protects the default branch with a ruleset, created with your own gh login: no direct pushes, only PRs, and a PR merges once its branch is up to date and each of those checks passes, from the App or from GitHub Actions.
For a job on GitHub whose check names init cannot tell, such as a job whose name or matrix comes from an expression, the ruleset keeps the checks the branch requires now. When gh cannot read the branch's rules, init makes no ruleset and leaves them as they are. Install the App before the first PR, since a required check that nobody posts blocks it. When your gh login has no admin rights on the repository, init says so and gives the link to set the ruleset up by hand, and the rest still applies. Update the ruleset when a job is added, renamed or removed. Require the checks has the manual steps.
Install
It writes the agent instructions (agents). Then, as shipcheck install does below, it removes the agent hooks an older version wrote, installs the git hooks for this clone, signs the machine in if it cannot post, and starts the daemon.
Other cases
- When
shipcheck.tomlalready exists,initleaves it as it is. - Without a terminal and without
--yes, as for an agent or a script that forgot the flag, it prints the plan and writes nothing: noshipcheck.toml, noAGENTS.md, no hooks, no daemon and no sign-in. Runshipcheck init --yesto apply it. - When every job stays on GitHub, or no check passes when init tries it, it writes no
shipcheck.toml, says why, and stops: no App, workflow or ruleset step, no agent instructions, hooks or daemon. - When it finds no check in the workflows or the project files, it writes
runner = "act"with every workflow inworkflows, run in containers, andshard_tests = true. Remove the workflows that must not run on a laptop, such as deploys. Modes compares the two. With no workflows and none of those project files, it stops with an error.
Add on = ["push", "commit"] to start runs on each commit too.
Commit it and merge it to main. shipcheck reads this file and the workflows only from main, so a PR cannot change what it must pass. Until the file is on main there is nothing to run: shipcheck run exits 2, and the hooks and the daemon start nothing. Every setting.
Your workflows keep running on GitHub on pushes to main, as a backstop after each merge.
Add the badge
To show that the repository's checks come from shipcheck, add this line to its README:
[](https://shipcheck.run)
For each teammate, once per clone
shipcheck install
It installs two hooks, pre-push and post-commit. The post-commit hook starts a run only when on has commit. It also starts the daemon as a background service, a launchd agent on macOS or a systemd user unit on Linux, and adds this repository to it. Set SHIPCHECK_NO_DAEMON=1 to skip that on one machine. Git never installs hooks on clone, so each clone needs this one step. To make it automatic, run it from the repository's usual setup: the prepare script in package.json, a just setup recipe, or a mise task.
If the repository already uses husky, lefthook or another hook manager, through core.hooksPath or a pre-push hook of its own, shipcheck install leaves its hooks alone and stops with an error that gives the line to add. It then does not sign the machine in or start the daemon, so run it again after you add the line. A post-commit hook of its own only gets a note with the line.
When the machine cannot post yet, install signs it in: in a terminal it prints a code to enter at github.com/login/device. Without a terminal it prints one line and goes on.
For an agent machine
- Install the binary.
- Run
shipcheck installin each clone. In a terminal, it signs the machine in if it cannot post yet.
A machine with nobody at its terminal, such as a VM, gets a token instead: run shipcheck token --name <machine> where you can open a browser and set the output as SHIPCHECK_TOKEN there (unattended machines).
Every day
Commit and push as usual. Nothing waits on shipcheck:
- Each commit or push starts a run in the background, as
onsays. - A newer commit on the same branch stops the older run.
- A tree that was already tested needs no run at all.
- The checks go on PRs, so
shipcheck commitandshipcheck postrefuse on the default branch.shipcheck runworks on any branch. - With the App installed and the machine signed in, a push is enough. The run it starts posts one check per job on the PR, such as
shipcheck/test, in progress at once and then green or red, and ashipchecksummary of the run. - When a PR changes on GitHub, such as a rebase with the Update branch button, the daemon reruns its checks (how it works).
To run the checks in the foreground and wait for the verdict, for example as an agent's test step before it pushes:
shipcheck run
When a run fails, give the agent the logs:
shipcheck logs