Exit Codes
Every sow command exits with one of seven codes. They are stable, they are the same for
every command, and they are meant to be branched on in scripts — the distinction between
“this failed” and “this was correctly refused” is the whole point of having more than one
nonzero code.
| Code | Meaning |
|---|---|
0 | Complete success, or an idempotent no-op |
1 | Runtime I/O, parser, renderer, or unknown internal error |
2 | Usage, workspace discovery, or configuration error |
3 | Partial success: at least one item committed, at least one failed |
4 | Write lock unavailable — held and --no-wait, or the timeout expired |
5 | Integrity or recovery error, or check judged the result not deliverable |
6 | Expected rejection: conflict, protected, no match, incompatible architecture |
Human-readable results go to stdout; warnings and diagnostics go to stderr. Each code has
a stable message prefix on stderr, and a matching class in
JSON output:
| Code | stderr prefix | JSON class |
|---|---|---|
1 | varies by subsystem | runtime |
2 | usage error: / workspace discovery error: / configuration error: | usage |
3 | ... batch partially succeeded | partial |
4 | lock unavailable: | lock |
5 | integrity or recovery error: | integrity |
6 | operation rejected: | rejected |
sow create is the exception to the prefix column: being outside the managed layer, it
prints its raw domain error on stderr (plain: scan …, plain: marker gate …) without the
operation rejected: prefix. The prefix is still present in its JSON errors[].message.
0 — Success or no-op
The command did what you asked, or found there was nothing to do. Both are success:
re-running sow create over an unchanged directory, or sow build on a clean repository,
exits 0 and says so.
sow create /srv/offline --json
{"schema":"sow.cli/v1","command":"create","ok":true,...,"result":{"dir":"/srv/offline","rpm":4,"deb":3,"kept":[...],"removed":[],"marker":false,"noop":true,"recovered":false},"errors":[]}
The "noop":true is how you tell a no-op from real work; the exit code does not
distinguish them.
sow status is a deliberate special case. As long as the state database is readable it
exits 0 in every repository state — clean, dirty, recovering, and error alike —
so a script can read the structured state instead of decoding an exit code. Use
sow check when you want a gate.
1 — Runtime error
Something went wrong at the I/O, parsing, or rendering layer: a directory that cannot be written, a disk that filled up, a package that cannot be read. These are environment problems, not usage problems.
chmod 500 /srv/readonly
sow create /srv/readonly
plain: create stage /srv/readonly: mkdir /srv/readonly/.sow-plain-stage-1457115008: permission denied
The staging directory is created up front precisely so this fails before anything is published. A repository that already had valid indexes still has them.
2 — Usage, discovery, or configuration
You asked for something the CLI cannot act on: an unknown flag, an ambiguous target, no
workspace, or a sow.yml that does not parse. Nothing was attempted.
An unknown option:
sow status --nope
usage error: unknown option "--nope"
Mutually exclusive options:
sow build -N -T 5s
usage error: --no-wait and non-zero --timeout are mutually exclusive
An ambiguous target — the repository has two distributions and the command needs one:
sow ls
workspace discovery error: managed: workspace discovery or configuration error: repository "pigsty" has multiple Dists (el9, trixie); select one or more with --dist
No workspace anywhere above the current directory — the message names where it searched and how to fix it:
workspace discovery error: managed: workspace discovery or configuration error: workspace not found (searched cwd="/home/vonng"); run sow init or set --workdir/SOW_DIR
A start directory that is not a real directory — a symlink, for instance, which is what
/tmp is on macOS — is refused before the search even begins:
workspace discovery error: managed: workspace discovery or configuration error: discover workspace from cwd "/tmp": start is not a directory
A malformed configuration file — note that the offending line is named:
sow config check
configuration error: load config "/srv/repo/sow.yml": parse sow.yml: yaml: unmarshal errors:
line 3: field repositories not found in type config.Config
Every syntax and schema error in sow.yml lands here.
3 — Partial success
A batch where some items were committed and some failed. This code exists so you never
have to guess whether a failed sow add left the repository untouched: with 3, the
valid packages are in, and the failed ones are named.
sow add ./incoming/ -d el9
add repository=pigsty operation=9162553676349401125 accepted=1 failed=1 memberships=+1/-0 revision=6 generation=6 dirty=false
item input="/incoming/broken-1.0-1.x86_64.rpm" status=failed error="invalid RPM package: parse RPM reader: unexpected EOF"
item input="/incoming/pgbouncer_fdw_18-1.4.0-1PGDG.rhel9.8.x86_64.rpm" status=reused format=rpm coordinate="pgbouncer_fdw_18-0:1.4.0-1PGDG.rhel9.8.x86_64" sha256:45171966... dists=el9:accepted
managed: batch partially succeeded
The failed input file is left exactly where it was. With --json, the committed items are
still listed in full — a nonzero exit never truncates the result:
{..., "ok":false, "result":{"accepted":1,"failed":1,"items":[...]}, "errors":[{"code":3,"class":"partial","message":"managed: batch partially succeeded"}]}
sow init uses the same code when it commits some declared repositories or distributions
and then fails on a later one.
4 — Lock unavailable
Another process holds the write lock. SOW is single-writer by design, so this is a normal, expected outcome — retry, or wait longer.
With --no-wait, the failure is immediate:
sow build -N
lock unavailable: managed: lock unavailable
With a timeout, it fails after exactly that long:
time sow build -T 2s
lock unavailable: managed: lock unavailable
real 0m2.016s
-T 0 — the default — waits indefinitely. Read-only commands never take a write lock and
never return 4; sow status even reports the contention as a field:
repository=pigsty status=clean ready_to_copy=false revision=7 generation=7 dirty_dists= pending=0/0 locked=true
5 — Integrity, recovery, or not deliverable
Two different situations share this code, and both mean “do not ship this tree yet”.
The common one is sow check on a repository whose desired state is ahead of what was
built — after sow add --skip, or after a policy or signing change. Every layer passes;
the repository is simply not converged:
sow rm 'rpm:pev2-0:1.23.0-1.noarch' -d el9 --skip
sow check
repository=pigsty status=dirty ready_to_copy=false revision=7 generation=6
config ok=true checked=5
state ok=true checked=1
public-modes ok=true checked=69
package-bytes ok=true checked=7
desired-membership ok=true checked=6
index ok=true checked=2
signature ok=true checked=11
generation-manifest ok=true checked=6
integrity or recovery error: managed: repository is not ready to copy: repository status is dirty
The fix is sow build. This is the code a deploy script should gate on — it is the
difference between “the tree on disk is complete and current” and “the tree on disk is
complete but stale”.
The rarer situation is genuine integrity failure: a state database, journal, and file tree
that contradict each other in a way SOW cannot safely resolve on its own. It refuses to
overwrite anything, and you restore from backup rather than forcing a repair. There is no
--force here on purpose.
6 — Expected rejection
The command was well-formed, the environment was fine, and SOW decided the answer is no. These are policy and safety decisions, not failures.
A protected repository:
sow repo rm pigsty -f
operation rejected: managed: operation rejected: repository "pigsty" is protected
A reference that matches nothing:
sow rm nosuchpkg -d el9
operation rejected: managed: operation rejected: package reference not found: package reference "nosuchpkg" matches no Desired Membership
An ambiguous bare name, with the candidates listed so you can pick one:
sow show libpq5 -d trixie
operation rejected: managed: operation rejected: package reference "libpq5" is ambiguous: deb:libpq5=18.2-1.pgdg12+1:amd64 sha256:310611d0..., deb:libpq5=18.3-1.pgdg12+1:amd64 sha256:4b526223..., deb:libpq5=18.3-1.pgdg12+1:arm64 sha256:cadeb929...
An architecture the workspace does not allow. Note that the per-item message names the detected value and tells you where to change it:
sow add ./centos-release-6-0.el6.centos.5.i686.rpm -d el9 --json
"items":[{"input":".../centos-release-6-0.el6.centos.5.i686.rpm","status":"failed",
"error":"managed: operation rejected: unknown rpm package architecture \"i686\"; supported rpm package architectures are [x86_64, aarch64, noarch] (canonical families [x86_64, aarch64, neutral]); use a supported package or update only supported architecture families in sow.yml"}]
A directory with nothing to index:
sow create /srv/empty
plain: scan /srv/empty: no supported top-level regular RPM or DEB packages
A --pigsty completion marker guarding an existing build:
sow create /www/pigsty
plain: marker gate /www/pigsty/repo_complete: repo_complete exists; use --pigsty or remove it explicitly before rebuilding
A signing key reference that parses but does not resolve — syntax errors are 2,
resolution failures are 6:
operation rejected: ... deb metadata key: key reference does not resolve to a bounded regular file
operation rejected: ... deb metadata key: environment key reference SOW_METADATA_KEY is unset
Using them in scripts
The codes are designed so a deploy pipeline can branch without parsing text:
#!/usr/bin/env bash
set -uo pipefail
sow add /incoming/*.rpm -r pigsty -d el9
case $? in
0) ;; # everything landed
3) echo "some packages rejected, continuing with what landed" >&2 ;;
4) echo "another writer holds the lock, retry later" >&2; exit 75 ;;
*) echo "add failed" >&2; exit 1 ;;
esac
# Gate the copy on a complete, current tree.
if ! sow check -r pigsty; then
echo "repository not ready to copy" >&2
exit 1
fi
rsync -a --hard-links --delete /srv/repo/pigsty/ mirror:/var/www/pigsty/
Two habits worth keeping: treat 4 as retryable rather than fatal, and never treat 6 as
a crash — it usually means your input, not SOW, needs to change.
See also
- JSON Output — the
errorsarray and itsclassfield sow check— the eight verification layers behind code5- Transactions & Recovery — what
recoveringanderrormean