The complete configuration schema: workspace, repository, distribution, membership policy, signing, and publication targets.
This is the multi-page printable view of this section. .
Reference
- 1: sow.yml Reference
- 2: Package References
- 3: Repository Layout
- 4: Exit Codes
- 5: JSON Output
- 6: Platforms & Integrations
This section is the stable contract for configuration fields, package references, paths, exit codes, JSON, platforms, and integrations. CLI syntax and state transitions live in Commands; use Get Started for the operating model.
Output examples show shape; identifiers, paths, hashes, timestamps, and counts vary by
workspace. The built-in sow help remains the exact syntax authority.
The five ways to name a package on the command line, how ambiguity is resolved, and
which forms rm, show, and where accept.
Every path SOW creates in plain and managed mode, the pool grouping rule, name constraints, and which directories must never be exposed over HTTP.
The seven exit codes and what each one means.
The sow.cli/v1 envelope, the meaning of each top-level field, and result shapes for the
primary command families.
Release targets, filesystem requirements, repository-client checks, publication Providers, and the exact scope of each automated integration.
Conventions
Command examples are written without a $ prompt so you can copy a whole block. Output
blocks are representative; variable values and long structures may be shortened where
marked. The built-in sow help remains the exact syntax authority shipped with a binary.
Placeholders in syntax blocks are uppercase (NAME, DIR, PACKAGE); literal text is
lowercase. Square brackets mark optional arguments, ... marks a repeatable one, and a
vertical bar separates alternatives — the same convention sow help uses.
1 - sow.yml Reference
sow.yml is the single configuration file of a managed workspace. It sits at the
workspace root, declares which repositories and distributions exist, and holds the
membership policy and signing settings that every build applies. Plain mode
(sow create) never reads it.
This page lists every field the parser accepts. Anything not listed here is rejected — there are no undocumented keys and no keys reserved for future use.
How the file is read
SOW parses sow.yml with a strict decoder. Practically, that means:
- Unknown fields are errors, not warnings. A typo like
repositories:instead ofrepos:fails the command with exit code2and names the offending line. - Exactly one YAML document. A
---separator introducing a second document is an error. - Regular file only. A symlink at
sow.yml, or a file larger than 16 MiB, is rejected before parsing. - Defaults are filled in at parse time, not written back to disk. Run
sow config show --allto see the fully expanded form.
Some of the file is machine-maintained. sow init, sow repo new, sow repo rm,
sow dist new, and sow dist rm rewrite sow.yml atomically as part of their
transaction. Membership policy and signing are yours to edit by hand; there are no CLI
flags that set them.
After any hand edit, run sow config check. It parses the file, cross-checks it against
the SQLite state of every initialized repository, and resolves every signing key
reference — without writing anything.
Top level
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
schema |
string | yes | — | Must be exactly sow/v3. Any other value is a configuration error. |
architectures |
list of strings | no | [x86_64, aarch64] |
The CPU families this workspace is allowed to manage. |
repos |
map | no | empty | Repository name to repository configuration. |
targets |
map | no | empty | Publication target name to target configuration. |
The configuration value must be exactly schema: sow/v3.
architectures
This is a ceiling, not a target. It declares which architectures SOW may accept at all; individual distributions inherit the whole list unless they narrow it.
Only two canonical families are supported today: x86_64 and aarch64. The DEB
ecosystem names are accepted as input aliases and normalized at the parse boundary:
| You may write | Stored and displayed as |
|---|---|
x86_64, amd64 |
x86_64 |
aarch64, arm64 |
aarch64 |
So architectures: [amd64, arm64] and architectures: [x86_64, aarch64] are the same
configuration. Writing both aliases of one family — [amd64, x86_64] — is a duplicate and
fails:
noarch (RPM) and all (DEB) are not architectures here. They are neutral packages,
projected into every applicable view at build time, and the parser rejects them in this
list. An unsupported value such as riscv64 fails immediately:
The list may be present or absent, but it may not be empty.
Repository
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
protected |
bool | no | false |
When true, sow repo rm refuses to delete this repository, even with -f. |
signing |
map | no | none | Package and metadata signing settings, see Signing. |
dists |
map | no | empty | Distribution name to distribution configuration. |
protected
protected: true is a guard against deleting a whole repository by accident. It blocks
exactly one thing — repository removal:
That is exit code 6. Everything else keeps working normally: you can still add, rm,
build, create and delete distributions. To actually remove a protected repository, edit
sow.yml to set protected: false, confirm with sow config check, then run
sow repo rm.
Repository names
Repository and distribution names share one grammar: they must match
[a-z0-9][a-z0-9._-]* — lowercase letters, digits, dot, underscore, hyphen, starting
with a letter or digit. Uppercase is rejected, because the name becomes a directory and
must behave identically on case-sensitive Linux and case-insensitive macOS filesystems.
These names are reserved and rejected: ., .., .sow, pool, dists, sow.yml,
workspace.lock, workspace-ops, repo-locks. Two repository names that would collide
in the state directory — say db and db.db — are also rejected:
See Repository Layout for why.
Dist
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
format |
string | yes | — | rpm or deb. A distribution holds exactly one format. |
architectures |
list of strings | no | inherits workspace list | Narrows this distribution to a subset of the workspace families. |
limit |
integer | no | 0 |
Maximum versions to keep per package name and architecture; 0 keeps all. |
exclude |
list of rules | no | empty | Rules that keep matching packages out of this distribution. |
format
format is the only field sow dist new sets from the command line, and it cannot be
changed afterwards — an RPM distribution never becomes a DEB one. A package whose format
does not match is simply not a candidate for that distribution.
architectures
Omit this field and the distribution inherits the workspace list, which is what you want
almost always. Declare it only to narrow: an el9 distribution that should be x86-only
in an otherwise dual-architecture workspace.
The list must be a subset of the workspace list, and it may not be empty:
Adding a family here marks the distribution dirty; the next sow build renders the new
view. Removing a family that is still referenced by existing membership or by the built
generation is refused by config check and by every write command.
limit
limit caps how many versions of one package survive in this distribution. The grouping
key is (binary name, native architecture), so an x86_64 build and an aarch64 build of
the same package are counted separately, and a noarch/all package forms its own group.
0(the default) keeps every version.N > 0keeps the newestN, comparing RPM EVR or Debian version with the native ordering rules of each ecosystem.- A negative value is a configuration error.
With limit: 1, adding an older version alongside a newer one reports it as limited and
does not create membership:
Raising limit later does not resurrect versions that policy previously removed. The
package bytes may still sit in the pool, but membership is gone; re-add the file to bring
it back. See Membership Policy for the reasoning.
exclude
exclude is a list of rules. Each rule is a set of fields; within a rule the fields are
ANDed, within a field the patterns are ORed, and rules are ORed with each other. A package
is excluded if any single rule matches it.
That reads: drop every debug-flavored package, and separately drop aarch64 packages whose
name starts with test- or ends with -experimental.
Five fields are allowed:
| Field | Matched against |
|---|---|
name |
Binary package name |
source |
Normalized source name (RPM SOURCERPM, DEB Source) |
arch |
x86_64, aarch64, or neutral |
kind |
The classification below |
format |
rpm or deb |
kind is derived from the binary package name by its most specific suffix:
| Format | Name suffix | kind |
|---|---|---|
| RPM | -debuginfo |
debuginfo |
| RPM | -debugsource |
debugsource |
| RPM | -llvmjit |
llvmjit |
| DEB | -dbgsym |
dbgsym |
| DEB | -dbg |
dbg |
| any | none of the above | main |
Patterns are case-sensitive: either an exact string or a shell glob using *, ?, and
[...]. There is no regex, no version comparison, no negation, and no expression syntax.
An empty rule, an empty or untrimmed pattern, a repeated pattern within one field, and an
invalid glob are all configuration errors:
Policy order is fixed: exclude runs first, then limit. An excluded package is reported
per item and is not a failure:
Signing
Signing settings live on the repository, not on individual distributions, and cover two independent trust chains: the packages themselves, and the repository metadata clients verify before they trust anything else.
The tree is fixed. signing.rpm has packages and metadata; signing.deb has
metadata only — DEB packages are never re-signed, because APT verifies the archive
through Release, not through per-package signatures.
rpm.packages
| Field | Type | Default | Meaning |
|---|---|---|---|
mode |
string | never, or fill when key is set |
never, fill, or always. |
key |
key reference | none | The signing key. Required unless mode is never. |
trusted_keys |
list of key references | empty | Additional public keys accepted by fill. |
The three modes:
never— input bytes are stored verbatim. Whatever signature the package arrived with (including none) is what clients get.fill— sign packages that have no signature, or whose signature is not verifiable bykeyor one oftrusted_keys. Packages that already verify keep their exact bytes.always— every package must end up signed bykey. Packages already signed by it keep their bytes; everything else is re-signed.
fill is the default when a key is present, because it is the mode that preserves
upstream signatures. Setting mode to fill or always without a key is an error:
trusted_keys is a list of public keys whose signatures fill accepts as already-good.
The public half of key is always trusted and does not need to be listed. Repeating the
same reference twice is an error:
RPM package signing is the one operation that shells out: SOW calls the environment’s
rpm --addsign / rpm --resign against a private staged copy, never against your input
file. The private key must be available to the GPG environment that rpm uses.
rpm.metadata and deb.metadata
| Field | Type | Default | Meaning |
|---|---|---|---|
key |
key reference | none | Key used to sign repository metadata. |
passphrase |
passphrase reference | none | Passphrase for a protected private key. |
Configure rpm.metadata.key and every RPM architecture view additionally publishes a
detached repodata/repomd.xml.asc. Configure deb.metadata.key and every DEB
distribution additionally publishes a clearsigned InRelease and a detached
Release.gpg. Without a key, those files are simply not produced — repomd.xml and
Release are always written.
For file:// and env:// references SOW signs in-process; no gpg binary is involved.
Only agent:// requires gpg in the environment.
Changing a key reference or the fingerprint behind it marks the affected distributions
dirty, because the signing identity is part of each distribution’s built configuration
digest. The next sow build re-signs and produces a new generation.
Key references
A key reference is a string in one of these forms:
| Form | Example | Notes |
|---|---|---|
| Path | keys/repo-signing.asc |
ASCII-armored key file. A relative path resolves against the workspace root, not your current directory. |
file://<path> |
file:///secure/repo-signing.asc |
Same as above, written explicitly. Absolute paths therefore show three slashes. |
env://<VAR> |
env://SOW_METADATA_KEY |
The variable holds the armored key material itself, not a path. The name must match [A-Za-z_][A-Za-z0-9_]*. |
agent://<fingerprint> |
agent://7F721C4AD40F...CF3B |
Delegates to the ambient gpg-agent. The fingerprint is 16, 40, or 64 hex digits, case-insensitive. |
Any other scheme is rejected:
References are validated in two stages. Syntax is checked when the file is parsed and
fails with exit code 2. Whether the reference actually resolves is checked by
sow config check and by every write command, and fails with exit code 6:
Secret material never leaves the reference. sow config show --all prints the reference
and the resolved fingerprint, and nothing else:
Private keys and passphrases are never written to sow.yml, SQLite, the operation log,
JSON output, or error messages.
Passphrase references
passphrase accepts the same path, file://, and env:// forms as a key reference —
but not agent://, since a passphrase is a value, not a key handle.
Two rules apply:
-
A passphrase without a key is an error. It has nothing to unlock.
-
A passphrase alongside an
agent://key is an error. The agent owns the private key and handles its own prompting; a second passphrase channel would be ignored.
Publication targets
Each target binds one configured Repository to a storage namespace. Target names use the same lower-case name grammar as repositories.
| Field | Required | Meaning |
|---|---|---|
repository |
yes | Existing Repository owned by this target. |
provider |
yes | filesystem or r2. |
endpoint |
yes | Canonical file:///absolute/path without trailing slash, or canonical https://host for R2. |
region |
R2 | Must be auto; forbidden for filesystem. |
bucket |
R2 | Lower-case canonical bucket name; forbidden for filesystem. |
prefix |
yes | Relative public-tree prefix; empty means the storage namespace root. |
credential |
R2 | env://NAME or file:///absolute/path; inline secrets are forbidden. |
public_endpoint |
yes | Canonical URL ending in /, used for public content/absence verification. Filesystem accepts https://, http://, or file://; R2 requires HTTP(S). |
max_cache_ttl |
yes | Canonical bounded non-negative Go duration, including explicit 0s; overflow is rejected. |
authoritative_workspace |
yes | Must be true. |
single_writer |
yes | Must be true. |
exclusive_write_authority |
yes | Must be true. |
The three authority booleans are explicit safety acknowledgements, not defaults. Targets on the same storage may not have overlapping prefixes; filesystem targets may not resolve to overlapping effective paths. These rules protect conditional publication and GC from competing writers.
For provider: filesystem, configuration validation checks URL shape and overlap. At
publication time the endpoint directory itself must already exist, must not be a symlink,
and must resolve to one canonical real directory. SOW creates the configured prefix below
that endpoint, not the endpoint itself.
The first publish durably binds the Repository, provider storage identity, and prefix. Later edits
to the target name, public_endpoint, or max_cache_ttl require explicit operator confirmation with
sow publish TARGET --rebind. Provider,
storage endpoint, region, bucket, prefix, and Repository identity cannot be rebound; configure a new
target instead. Every accepted rebind appends an immutable binding revision. Pending maintenance
blocks TTL changes, and filesystem conditional-delete maintenance also blocks a public-endpoint
change.
R2 credentials are private references. The environment variable value or referenced file must contain one strict JSON document, not a path or shell assignment:
An optional temporary credential may add "session_token":"...". Unknown fields,
trailing data, missing access/secret values, and documents larger than 64 KiB are rejected.
config show, JSON output, and the public tree never contain the credential material.
Complete example
A workspace with two repositories: a protected production repository signing both metadata chains and filling in missing RPM signatures, and a scratch repository with no signing and no policy.
Validate it before you rely on it:
What is not in sow.yml
Some things you might expect to configure are deliberately not configurable:
- Repository paths. A repository always lives at
<workspace>/<name>. There is nopath:field. See Repository Layout. - APT components. Always
main. YUM has no component concept. - Architecture views. Derived from
architecturesand the package headers, never declared per package. - Inline secrets. Targets accept only credential references; key and passphrase material likewise stays behind a reference.
- Automatic retention counts. Retention is an explicit
sow retain add/rmoperation, not a rolling count in configuration.
See also
sow config— the commands that read this file- Membership Policy — how
excludeandlimitbehave over time - Signing Model — the two trust chains explained
publish,retain,gc, andexport— delivery lifecycle commands- Exit Codes — what
2and6mean here
2 - Package References
sow rm, sow show, and sow where all take a PACKAGE argument. This page defines
what you may write there. The same grammar applies to all three commands; only the
handling of an ambiguous name differs.
Nothing here applies to sow add, which takes filesystem paths, not references.
The five forms
| Form | Example | Matches |
|---|---|---|
| Content digest | sha256:d06d7f23b9cf...b98b1229 |
Exactly one package object |
| RPM coordinate | rpm:pev2-0:1.23.0-1.noarch |
Exactly one RPM |
| DEB coordinate | deb:libpq5=18.3-1.pgdg12+1:amd64 |
Exactly one DEB |
| Filename | pev2-1.23.0-1.noarch.rpm |
The package stored under that filename |
| Bare name | pev2 |
Every version and architecture of that name |
The first three are exact: they name one object and either hit it or fail. The last two are conveniences that may match more than one object.
You never have to construct these by hand. sow ls prints the digest and the coordinate
of every package, in a form you can paste straight back into another command:
Content digest
The SHA-256 of the complete stored package bytes. This is the strongest reference SOW has: it is the object’s identity, so it can never be ambiguous.
The digest must be complete and lowercase. There is no prefix matching and no case-folding — a short or uppercase digest is a usage rejection, not a failed lookup:
Note that this digest covers the bytes as stored. If a repository re-signs RPM
payloads, the digest of the object differs from the digest of the file you handed to
sow add.
RPM coordinate
The full NEVRA, prefixed with rpm:. Every component is required, including the epoch —
0 when the package has none.
Quote it in a shell: NEVRA contains a colon, and history expansion or path completion can otherwise mangle it.
Both the prefix and the epoch are load-bearing. Dropping either turns the string into a bare-name lookup that finds nothing:
The architecture component is the one from the RPM header: x86_64, aarch64, or
noarch. It is not the canonical family — a noarch package is written noarch here,
even though SOW classifies it internally as neutral.
DEB coordinate
The Debian identity triple, prefixed with deb:. The version is the complete Debian
version including epoch and revision; the architecture is the ecosystem name (amd64,
arm64, all), not the canonical family.
All three parts are required. deb:libpq5=18.3-1.pgdg12+1 without an architecture does
not match anything.
Filename
The complete filename of the package as stored, including the extension:
This is the easiest form to type when you are looking at a directory listing. It is not an identity, though: filename is not what SOW uses to tell packages apart, and two distinct objects could in principle carry the same name. Prefer a coordinate or a digest in scripts.
Bare name
Just the binary package name:
What this means depends on the command:
-
sow rmtreats it as every version and native architecture of that name in the selected distributions. This is intentional — removing a package usually means removing all of it. Preview first with-c: -
sow showandsow whererequire it to identify exactly one object. They describe a single package, so a name matching several is refused with the candidate list:Every candidate is printed with both its coordinate and its digest, so the fix is to copy one of them back onto the command line.
What does not work
A NEVRA without the rpm: prefix looks like a coordinate but is parsed as a bare name,
and bare names do not contain epochs or architectures:
There is also no glob, no regex, no version range, and no --all flag. If you want to
select a set of packages by pattern, that is membership policy
in sow.yml, not a command-line selector. The command line only ever names packages that
already exist.
Scope
A reference is resolved within a scope, and the scope is set by the usual selection flags, not by the reference:
| Command | Default scope | Narrow with |
|---|---|---|
sow rm |
The selected distributions of the selected repository | -r, -d (required when several exist) |
sow show |
The selected repository | -r, -d |
sow where |
Every repository in the workspace | -r, -d |
sow where is the one that searches broadly — use it when you know a package exists
somewhere but not where. sow show describes one object in one repository in full detail.
The two commands also word their misses differently, which tells you which one you ran:
Coordinates and identity
The coordinate forms above are the logical identity of a package, and SOW enforces that
one coordinate maps to at most one content object inside a repository. Adding a different
file under a coordinate that already exists is a hard conflict — SOW will not silently
pick a winner, and there is no --replace.
Two packages that differ only in signature therefore still collide, because the coordinate
is the same. If you re-sign a package for real, bump its release; if you are re-adding the
identical input, SOW recognizes it and reports reused.
See also
sow rm— removal, preview, and batch semanticssow ls,show, andwhere— the query commands- Exit Codes —
6covers both “no match” and “ambiguous”
3 - Repository Layout
SOW has one fixed Managed layout: package payloads live once under pool/, while
dists/ contains metadata-only client views. The complete repository directory is the
unit to serve, copy, or publish.
Plain mode
sow create writes indexes next to existing packages and leaves every unrelated file
unchanged:
Flat RPM metadata uses a bare package basename; flat DEB metadata uses ./<filename>.
While a build is active, .sow-plain-stage-* contains private generated output. Plain has no
durable journal or recovery state; a later create discards stale reserved temporary paths and
rebuilds. Never serve or copy those temporary paths.
Managed workspace
Repositories do not deduplicate across repository boundaries. .sow/ and the pending
directory are private (0700). Pending payload files use their final public mode (0644),
so promotion can be a namespace-only operation. Private state may contain unpublished bytes,
credentials-derived state, and recovery data.
<repo>.db and its rebuildable package-facts cache are private. Neither changes the public
repository layout or the sow/v3 configuration identifier.
SOW 0.4 uses internal database schema v12. Its append-only publication-target binding revisions,
package facts, signer projections, and recovery evidence all remain inside <repo>.db. A v0.3
database must be backed up and upgraded with sow repo migrate; never edit PRAGMA user_version
or copy the database without its matching public/private Repository state.
Canonical pool
Each package payload has one canonical path:
The source comes from RPM SOURCERPM or DEB Source; SOW falls back to the binary
package name when the source is absent. The prefix is the first lower-case character,
or the first four characters for names beginning with lib:
| Source | Example |
|---|---|
postgresql-18 |
pool/p/postgresql-18/libpq5_18.3-1.pgdg12+1_amd64.deb |
blackbox_exporter |
pool/b/blackbox_exporter/blackbox_exporter-0.28.0-1.x86_64.rpm |
libfoo |
pool/libf/libfoo/libfoo1_1.0-1_amd64.deb |
Pool objects are immutable. Removing distribution membership does not immediately remove
their bytes; unreachable payloads are handled by sow gc only after every safety root —
current, retained, recovery, publication, and any active maintenance operation — has been
considered.
RPM metadata-only views
There is no dists/<dist>/<arch>/pool/. Native packages appear only in their matching
architecture metadata; noarch packages appear in every architecture view. rpm-md
points back to the canonical pool:
The layout requires a client that honors relative rpm-md locations across the complete
Repository root. Default dnf reposync rejects the parent-traversing href because its
download destination escapes the view root. When a downstream tool requires a
self-contained leaf, create one explicitly with:
The export has a local pool/ and rewritten hrefs; it is a compatibility artifact, not
the canonical managed repository.
DEB views
Packages records refer to the same canonical pool from the archive root:
Release uses SHA256 manifests and advertises Acquire-By-Hash: yes. Checksum-named
rpm-md files and APT by-hash entries keep the preceding metadata reachable while the
mutable pointer is replaced last.
Publication targets
Both filesystem and r2 targets receive the same logical public tree beneath their
configured prefix:
The published unit is always the whole repository namespace. Do not publish only one RPM architecture directory because its hrefs deliberately refer to the root pool.
Names and serving boundary
Repository and distribution names must match [a-z0-9][a-z0-9._-]*. ., .., .sow,
pool, dists, sow.yml, workspace.lock, workspace-ops, and repo-locks are
reserved where applicable. Case-insensitive pool-path collisions are rejected so output
remains portable between Linux and default macOS filesystems.
Point your web server at <workspace>/<repo>/, not at the workspace root. The public
repository needs both pool/ and dists/; the private .sow/ directory must stay hidden.
See also
4 - 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.
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.
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:
Mutually exclusive options:
An ambiguous target — the repository has two distributions and the command needs one:
No workspace anywhere above the current directory — the message names where it searched and how to fix it:
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:
A malformed configuration file — note that the offending line is named:
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.
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:
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:
With a timeout, it fails after exactly that long:
-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:
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:
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:
A reference that matches nothing:
An ambiguous bare name, with the candidates listed so you can pick one:
An architecture the workspace does not allow. Note that the per-item message names the detected value and tells you where to change it:
A directory with nothing to index:
A --pigsty completion marker guarding an existing build:
A signing key reference that parses but does not resolve — syntax errors are 2,
resolution failures are 6:
Using them in scripts
The codes are designed so a deploy pipeline can branch without parsing text:
Here mirror is a configured publication target for 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 ordered verification layers behind code5- Transactions & Recovery — what
recoveringanderrormean
5 - JSON Output
Every command that produces data accepts --json. The output is a single line on stdout
carrying a versioned envelope, so you can pipe it straight into jq without worrying
about which command produced it.
(Line-wrapped here for readability; the real output is one line.)
The envelope
| Field | Type | Meaning |
|---|---|---|
schema |
string | Always sow.cli/v1. Check it before parsing anything else. |
command |
string | The command as invoked, including the subcommand: add, repo ls, config show. |
ok |
bool | true when errors is empty. Equivalent to exit code 0. |
repository |
string or null | The selected repository, or null for workspace-wide and plain-mode commands. |
operation |
string or null | The operation ID for write commands, null for read-only ones. |
result |
object or null | Command-specific payload, described below. |
errors |
array | Zero or more {code, class, message} objects. |
All seven fields are always present. result is null when the command failed before meaningful
work — an unknown flag, discovery failure, or invalid configuration before a Repository was
selected, for example. Committed partial results and diagnostic results from check or
rm --check are preserved even when the command exits nonzero.
errors
| Field | Meaning |
|---|---|
code |
The process exit code — 1 through 6. |
class |
runtime, usage, discovery, config, partial, lock, integrity, or rejected. discovery and config are stable specializations of exit code 2. |
message |
The same text written to stderr. |
Branch on class, not on message text. Messages carry paths and package names and will
change; the class will not.
When a batch partially succeeds, ok is false and result lists everything that was
committed. Never discard the payload because the exit code was nonzero — for add, that
is exactly where you learn which packages landed.
Operation IDs are strings
Operation IDs are 64-bit values serialized as decimal strings, because they routinely
exceed what an IEEE 754 double can represent exactly. In JavaScript, JSON.parse on a
bare number would silently corrupt them. Keep them as strings; jq handles them
correctly as-is.
Generation IDs are fixed-width strings
Generation IDs cover the full unsigned 64-bit domain and are serialized as exactly 20
zero-padded decimal digits. Treat generation, built_generation, base_generation,
and Generation-valued base fields as strings. The fixed width preserves numeric order
under ordinary bytewise comparison.
stdout and stderr
Results and the JSON envelope go to stdout. Warnings and error diagnostics go to stderr,
in addition to appearing in the errors array. So this works:
Result shapes
create
| Field | Meaning |
|---|---|
dir |
The absolute directory that was indexed |
rpm, deb |
Package counts per format |
kept |
Filenames included in the indexes, sorted |
removed |
Packages deleted by --pigsty cleanup; empty otherwise |
marker |
Whether repo_complete was written |
marker_sha256 |
Digest of the marker file; present only with --pigsty |
noop |
true when the indexes were already correct and nothing changed |
recovered |
Reserved for schema compatibility; Plain create has no journal recovery and always reports false |
signed |
Filenames re-signed; present only with --sign-with |
init
On a rerun over a workspace that already exists, the counters are 0 and existing
names what was found:
config check and config show
config show returns the effective configuration itself, in the same shape as
sow.yml after normalization:
With --all, signing entries additionally carry key_fingerprint. Private key material
and passphrases never appear.
repo ls, repo new, repo show
repo ls returns an array; repo new and repo show return one object of the same
shape.
packages counts distinct package objects in the pool; memberships counts
distribution memberships, so a package in two distributions counts once and twice
respectively.
repo rm returns only the outcome:
dist ls, dist new, dist show
Each architecture entry carries both names: family is the canonical form used in
configuration, ecosystem_arch is what appears in the published tree — identical for RPM,
amd64/arm64 for DEB.
desired_members ahead of built_members, or dirty: true, means a sow build is
pending. effective_config_sha256 is the digest of everything that feeds the renderer;
when it changes, the distribution becomes dirty.
dist rm mirrors repo rm: {"name":"el9","noop":false,"removed":true}.
add
One items entry per input path, in stable order. status is the item’s overall
outcome, and dists gives the per-distribution decision:
status |
Meaning |
|---|---|
accepted |
New package object, membership created |
reused |
The identical object already existed; may still add membership elsewhere |
excluded |
Kept out by policy — see dists for whether it was excluded or limited |
failed |
Not admitted; error carries the reason |
The per-distribution values are accepted, excluded, and limited. A package can be
accepted by one distribution and limited by another in the same command:
A failed item carries error instead of the package fields:
memberships_added and memberships_removed count both sides, because limit can evict
older versions in the same operation that admits a new one.
rm
check is true when the command ran with -c/--check, in which case nothing was
written and changes is a forecast. Note that removed lists membership removals only —
pool bytes are never deleted by rm.
build
noop: true means the desired state already matched the built tree, so no generation was
created. dists lists the distributions considered, not necessarily the ones rebuilt.
status
status is one of clean, dirty, recovering, error. ready_to_copy is the field
to read in a deploy script — but remember status exits 0 in every state, so test the
field, not the exit code:
pending counts package bytes held privately after add --skip, not yet published.
repository_locked reports whether another process currently holds the write lock.
check
Steady-state checks return nine layers in this order, each with a count of what it examined and any
issues found. A non-terminal layout transition instead returns config, state, public-modes,
and layout-transition, then stops with a not-ready result. A dirty repository can report every steady-state
layer ok: true and still fail with exit 5,
because the layers verify consistency while ready_to_copy reports currency:
changes
| Field | Values |
|---|---|
op |
add, update, delete |
phase |
payload, metadata, pointer, delete |
path |
Always relative to the repository root, always /-separated |
size, sha256 |
Present for add and update; omitted for delete |
Apply the phases in that order and no client ever sees a dangling reference: package
bytes first, then checksum-named metadata, then the protocol pointers (repomd.xml,
Release), and only then the deletion of superseded files.
sow changes 0 yields the complete current tree as one add set — a full delivery
manifest.
ls, show, where
ls returns an array of package objects; show returns exactly one under package.
The fields worth knowing:
| Field | Meaning |
|---|---|
architecture |
As it appears in the package header: x86_64, noarch, amd64, all |
canonical_arch |
The family SOW groups by: x86_64, aarch64, or neutral |
payload_sha256 |
RPM only — the signature-neutral digest used to recognize re-signed copies |
signature_key |
Key ID of the embedded signature, when the package carries one |
storage |
pool when published, pending when added with --skip |
dists / built_dists |
Desired membership versus what the last build published |
dists longer than built_dists is another way to see that a build is pending.
where searches the whole workspace and returns locations instead of full objects:
publish, retain, gc, export
Managed lifecycle commands use the same envelope and keep numeric Generation values as JSON strings:
| Command | Important result fields |
|---|---|
publish |
repository, target, provider, generation, attempt, checkpoint, phase, objects, noop |
publish --abort |
repository, target, provider, attempt, phase, objects |
publish --rebind |
Same result as publish; the binding revision is durable private audit state, not an extra wire field |
retain add / retain rm |
repository, record, record_identity, path |
retain ls |
repository, generations[] with the same retained record shape |
local gc |
operation, repository, base_generation, generation, objects, bytes, noop |
target gc |
repository, target, provider, phase, reports, candidates, deleted_objects, deleted_bytes, retained_objects, pending_grace, completed_attempts, noop |
export rpm-leaf |
repository, repository_id, generation, dist, arch, directory, method, signed, signer_identity, packages, files, manifest_sha256 |
An optional identity such as attempt, checkpoint, or local-GC operation is omitted
when there is no value. R2 target GC reports candidates as retained and never reports
remote deletion performed by SOW.
log
sow log returns the operation ledger, newest first:
payload_json and result_json are strings containing nested JSON, not objects. They are
stored verbatim so the audit record is byte-stable; parse them with a second pass:
Passing an operation ID returns the full detail — state transitions, structured
build_progress events, packages, memberships, and every file action:
sow log prune returns what it removed:
Note that before echoes the absolute timestamp a bare date resolved to in your local
timezone.
log export is not an envelope
sow log export writes JSON Lines — one complete operation record per line, no
envelope, no --json flag. It is meant for archiving, not for scripting a single command:
It refuses to overwrite an existing file, and it refuses a target whose parent directory is a symlink.
A worked example
Fail a deploy unless the repository is both consistent and current, then list exactly what to copy:
See also
- Exit Codes — the
codeandclassvalues inerrors - CLI Commands — which commands accept
--json - Observability & Audit — what the operation ledger records
6 - Platforms & Integrations
This page defines the environments SOW ships for, the storage semantics it requires, and the exact scope of its automated integrations. Repository generation happens inside the SOW binary; a real package manager remains the final check for a deployed repository.
Release targets
| Operating system | amd64 |
arm64 |
Artifact |
|---|---|---|---|
| Linux | yes | yes | archive, RPM, DEB |
| macOS | yes | yes | archive |
| Windows | no | no | not supported |
Release binaries use CGO_ENABLED=0 and require no language runtime. The 0.4.0 artifacts were
built with Go 1.27.0; the source module requires Go 1.27.0 or newer. Archives include README.md,
CHANGELOG.md, and the Apache-2.0 LICENSE;
Linux packages install the same license with the binary. Use sow version
to print the product version, target OS/architecture, and build toolchain.
Workspace filesystem
Managed workspaces belong on a local POSIX filesystem. Correctness depends on advisory
locks, fsync, descriptor-bound path checks, and atomic same-filesystem rename. NFS and
other network filesystems are not supported workspace locations.
The public <workspace>/<repo>/ tree is different: it is a closed pool/ + dists/
namespace designed for whole-root copying and publication. It does not depend on SQLite,
private journals, or view-local hard-link identity. Keep the complete Repository together
and never expose .sow/.
SOW rejects symlinked control paths, unsafe regular files, overlapping filesystem targets, and case-folded pool-path collisions. This keeps one Repository portable between case-sensitive Linux filesystems and the default case-insensitive macOS setup.
Automated integration matrix
| Surface | Environment | Verified behavior |
|---|---|---|
| Production CLI clean room | Linux CI | Builds the shipping binary; creates mixed Plain RPM/DEB metadata; initializes sow/v3; creates RPM and DEB Dists; adds fixtures; runs query, build, check, changes, config, and log commands |
| Plain APT client | Ubuntu 22.04 container | Serves sow create output over HTTP; runs apt-get update, package discovery, exact-version selection, download, and install with an explicitly trusted unsigned source |
| RPM detached-signature transition | AlmaLinux 8, 9, and 10 containers | Runs real DNF clients against serial repomd.xml / repomd.xml.asc transition states and pins which combinations succeed or fail |
| S3-compatible transport | Pinned MinIO container | Exercises bucket listing, HEAD, GET, create-only/CAS writes, replay, conditional multipart upload, object metadata, retries, and prefix confinement |
| Release packaging | Linux CI | Builds four archives, two RPMs, two DEBs, and SHA256SUMS; checks package paths, Apache-2.0 metadata, and packaged license bytes |
The DNF signature-transition probe is a protocol test, not a complete Managed RPM install. The APT job covers an unsigned Plain repository, not Managed metadata signing. Run the exact dnf/APT version, repository URL, access policy, and signing policy used by your deployment before promoting it.
Repository client contract
Plain RPM repositories expose repodata/ beside package files. Plain DEB repositories
expose Packages and Packages.gz beside package files. They can be consumed through
file:// or HTTP after the client trust policy is configured.
Managed clients consume the complete Repository root:
- APT indexes live below
dists/<dist>/main/binary-<arch>/and refer to the rootpool/.Releaseadvertises SHA-256 by-hash indexes; configured signing addsInReleaseandRelease.gpg. - RPM metadata lives below
dists/<dist>/<arch>/repodata/and uses relative locations that point back to the rootpool/. Serve the whole Repository, not one architecture directory.
Default dnf reposync rejects the canonical Managed RPM parent-relative package paths.
For that workflow, generate a self-contained copy with
sow export rpm-leaf. The export has local package paths and a
completion manifest; it is not a second canonical Repository.
Publication Providers
| Provider | Contract |
|---|---|
filesystem |
Publishes beneath a pre-existing safe file:// endpoint. Target GC performs exact conditional deletion only after cache grace and storage/public absence evidence. |
r2 |
Publishes through the S3-compatible storage transport. Target GC writes exact report-only candidate records and never deletes remote objects. |
Both Providers publish the same complete pool/ + dists/ namespace beneath the configured
prefix. public_endpoint is part of target verification; SOW does not create an HTTP
server, DNS record, bucket policy, CDN, or credentials. Validate those deployment-owned
surfaces on a nonproduction prefix before enabling production publication.
Filesystem and R2 HTTP(S) endpoints share the same canonical-GET content verifier. Filesystem
targets may also use descriptor-bound file:// verification. R2 public endpoints must be HTTP(S).
Target name, public_endpoint, and max_cache_ttl may be changed only through explicit
publish --rebind; storage identity and
prefix are immutable.
Deployment gate
Before delivery, require a clean deep check and inspect the physical change plan:
After publication, fetch the actual repomd.xml or Release URL and run the target package
manager. A local build, a Provider write, HTTP reachability, and a client install are
separate checks.
See Repository Layout, Signing, and Publication & Recovery for the corresponding contracts.