Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6. Build, test, and verify

You can compose a fleet, pin it, and ship changesets across it. Now let’s make it earn its keep day to day. The promise of a multi-repo tool is simple: do something across every repo at once, in parallel, without a hand-rolled for loop — and then gate the whole fleet so CI stays green.

In this chapter you’ll run arbitrary commands across the fleet, wire up build and test, search every repo in one shot, and use verify as the drift gate that ties it together. Keep the my-first-stack workspace open.

Running commands across the whole fleet in parallel

One command in, the whole fleet processed in parallel — no hand-rolled for loop.

🎯 In this chapter, you’ll learn to…

  • Fan any command across every repo with haw run.
  • Declare build = / test = per repo and drive the whole fleet with haw build / haw test — live-streamed, per-repo color.
  • Search every repo at once with haw grep.
  • Gate the fleet against the lock with haw verify — the CI drift gate.
  • Control the blast radius with -j (parallelism) and –group (scope).

haw run fans a command across every repo in parallel; verify gates the fleet against the lock.

⚙️ 1. Run any command across every repo — haw run

The workhorse. haw run takes a command positionally and runs it in every repo, in parallel:

haw run 'git log -1 --oneline'
── hello-world ──
7fd1a60 Merge pull request #6 from Spaceghost/patch-1
── spoon-knife ──
d0dd1f6 Pointing to the guide for forking
ran in 2/2 repos

haw groups the output per repo so you always know which repo said what. Anything you’d type in one repo works: git fetch, git status -s, a linter, a shell one-liner.

Tip: Quote the command. haw run 'git status -s' passes the whole string as one command; without quotes your shell would try to interpret the flags itself.

🔨 2. Declare build and test — then run them fleet-wide

haw stays build-system-agnostic: each repo names the shell command that builds or tests it, and haw fans those out. You declare them in the manifest with build = and test =.

In the real world each repo brings its own toolchain. Here’s the shape, from the shipped microservices example:

[repo.proto]
build = "buf generate"
test  = "buf lint"

[repo.gateway]
build = "go build ./..."
test  = "go test ./..."

[repo.billing]
build = "cargo build --release"
test  = "cargo test"

[repo.accounts]
build = "npm ci && npm run build"
test  = "npm test"

Our octocat repos have nothing real to compile — and if you run haw build now, it tells you so and exits non-zero:

$ haw build
error: no cloned repo declares a `build` command in the manifest

So let’s declare trivial commands to see the mechanism. Add a build = and test = line to both repos in your my-first-stack/haw.toml:

[repo.hello-world]
remote = "gh"
repo = "Hello-World.git"
rev = "master"
groups = ["core"]
build = "echo built"
test  = "echo tested"

[repo.spoon-knife]
remote = "gh"
repo = "Spoon-Knife.git"
rev = "main"
groups = ["web"]
build = "echo built"
test  = "echo tested"

Now two commands drive the whole fleet:

haw build     # runs every repo's `build =`, in parallel
haw test      # runs every repo's `test =`, in parallel
$ haw build
── hello-world ──
built
── spoon-knife ──
built
build ran in 2/2 repos

Output is live-streamed as each repo runs and grouped under a per-repo header, so on a color terminal each repo’s stream carries its own color — you can read a fleet-wide build without losing track of which repo produced which line. Repos that don’t declare the command (or aren’t cloned) are simply skipped. And here’s the detail that makes this a CI building block:

CI building block: haw build and haw test exit non-zero if any repo fails. That’s why the same command you run locally drops straight into a pipeline — the pipeline stops when any repo’s build breaks.

🔎 3. Search the whole fleet — haw grep

Need to find every use of a symbol, a TODO, a deprecated API across all repos? haw grep fans git grep across every cloned repo at once:

haw grep guide
spoon-knife (1 hit(s))
  README.md:9:For some more information on how to fork a repository, [check out our guide, "Forking Projects""](http://guides.github.com/overviews/forking/). Thanks! :sparkling_heart:
1 hit(s) in 2 repo(s) for `guide`

Each repo with a match gets a header and its hits underneath, and a final line totals the hits and repos searched — a repo with no match just doesn’t appear. It searches tracked files via Git, so it’s fast and ignores your build artifacts for free. Scope it to one stack with --stack <name> when the fleet is large.

🛡️ 4. Verify — the CI gate

Building and testing green is only half the story. Before any of it, CI must prove the tree is the one you pinned — no drift from haw.lock. That’s haw verify, which you met in Chapter 3:

haw verify; echo "exit code: $?"
verified: tree matches haw.lock (2 repos)
exit code: 0

verify asserts the on-disk tree matches the lock and exits 3 on drift — a clean, scriptable gate. It’s the first real step of every pipeline: sync to the lock, verify you’re on it, then build and test. Chapter 7 wires the full sync → verify → build → test job.

Tip: verify reads the tree and the lock only — no network, no builds. It’s cheap enough to run as a git pre-commit hook (haw hooks install) so drift never even reaches CI.

🎚️ 5. Control the blast radius: parallelism and groups

Two levers keep fleet-wide commands under control.

Parallelism — -j. By default haw runs up to min(cores, 8) repos at once. Cap it when a task is heavy (or a CI runner is small):

haw test -j 4     # at most 4 repos building/testing at a time
haw run -j 1 'git fetch'   # fully serial

Groups — --group. Remember the groups = [...] labels from your manifest? They exist precisely so you can act on a slice of the fleet. In my-first-stack, hello-world is in group core and spoon-knife is in web:

haw status --group core
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  master                    7fd1a60b   -      -

Only the core repo. The same --group filter works on sync, status, and run (and build/test), and it’s repeatable — pass it twice to select two groups. An empty filter means everything; a filter excludes ungrouped repos.

haw run --group core 'git log -1 --oneline'   # only core repos

🗺️ 6. When to reach for each

A quick map so you pick the right verb without thinking:

You want to…UseWhy
run an ad-hoc command everywherehaw run '<cmd>'one-off, not part of the manifest
build the whole producthaw buildruns each repo’s declared build =, CI-ready exit code
test the whole producthaw testruns each repo’s declared test =, fails on any failure
prove the tree matches the lockhaw verifydrift gate, exit 3 on drift
find text across all reposhaw grep <pat>fleet-wide git grep, grouped output
limit to part of the fleetadd --group <g>act on a labeled slice
tame a heavy or CI runadd -j <n>cap concurrent repos

The rule of thumb: run for one-off commands, build/test for the commands your repos declare, verify to gate. The declared ones are the ones you’ll want identical locally and in CI.

🙌 Your turn

Put the fleet through its paces in my-first-stack:

  • Add the trivial build = / test = lines above, then run haw build and haw test — watch the per-repo streamed output and the 2/2 repos summary.
  • Run haw grep guide across the fleet (a real hit in spoon-knife). Then narrow it: haw run –group core ‘git log -1 –oneline’ — only the core repo should answer.
  • Run haw verify; echo $? and confirm exit 0 on a clean tree. Force it fully serial with haw run -j 1 ‘git status -s’ and watch the repos process one at a time.

✅ Recap

  • haw run '<cmd>' runs any command in every repo in parallel, output grouped per repo.
  • Declare build = / test = per repo; haw build / haw test fan them out, live-stream per-repo output, and exit non-zero on any failure — so they double as CI steps.
  • haw grep <pattern> is a fleet-wide git grep.
  • haw verify gates the tree against haw.lock (exit 3 on drift) — the first move of every pipeline.
  • -j N caps parallelism; --group G (repeatable) scopes commands to labeled repos.

👉 Next

You can build, test, and gate the fleet locally. Now let’s make all of it production-grade — trust, CI, signing, and audit → 7. Going to production.