This is the multi-page printable view of this section. .
Commands
- 1: sow create
- 2: sow init
- 3: sow config
- 4: sow repo
- 5: sow dist
- 6: sow add
- 7: sow rm
- 8: sow ls
- 9: sow show
- 10: sow where
- 11: sow status
- 12: sow build
- 13: sow check
- 14: sow changes
- 15: sow publish
- 16: sow retain
- 17: sow gc
- 18: sow export
- 19: sow log
Each top-level command has its own page. Command groups
such as config, repo, dist, retain, export, and log document their subcommands together.
The built-in sow help output is the syntax authority. These pages add selection rules, state
transitions, output contracts, failure behavior, and practical examples.
Command index
sow create is the Plain-mode repository command and operates directly on a directory. sow init
bootstraps Managed mode and may create sow.yml; the remaining stateful commands discover an
existing Workspace. help and version are utility commands and need neither mode.
| Command | Mode | Purpose |
|---|---|---|
sow create [DIR] |
Plain | Generate a flat RPM/DEB repository in place |
sow init [DIR] |
Managed | Initialize a Workspace and converge declared Repositories/Dists |
sow config check|show |
Managed | Validate or print the effective configuration |
sow repo ls|new|show|migrate|rm |
Managed | Manage Repositories; migrate is specialized maintenance |
sow dist ls|new|show|rm |
Managed | Manage Dists |
sow add PATH... |
Managed | Add packages to Desired Membership |
sow rm PACKAGE... |
Managed | Remove packages from Desired Membership |
sow ls |
Managed | List Desired and Built Membership |
sow show PACKAGE |
Managed | Inspect one Package Object |
sow where PACKAGE |
Managed | Locate a Package Object across the Workspace |
sow status |
Managed | Read Repository state without deep verification |
sow build |
Managed | Converge Desired state into a Built Generation |
sow check |
Managed | Verify configuration, state, bytes, views, signatures, and manifest |
sow changes [BASE_GENERATION] |
Managed | Diff Built Generations as a file delivery plan |
sow publish TARGET |
Managed | Publish a verified Generation to a configured target |
sow retain add|ls|rm |
Managed | Manage explicit retained-Generation roots |
sow gc [TARGET] |
Managed | Collect unreachable local payloads or maintain a publication target |
sow export rpm-leaf |
Managed | Build a standalone RPM compatibility leaf |
sow log [OPERATION] |
Managed | Read, export, and prune the Operation audit ledger |
Global syntax
Running sow with no arguments prints the command list and exits 0. Use sow help COMMAND or
sow help COMMAND SUBCOMMAND for built-in usage. sow version and sow --version print the binary
identity.
There is no global --format, --yes, --dry-run, -q, -v, or --config. Unknown flags are
usage errors.
Workspace discovery
Managed commands find the nearest ancestor containing sow.yml:
- Start at
-C/--workdir DIR, when supplied; otherwise start at the current directory. - Search upward and stop at the first
sow.yml. - If that search finds nothing, repeat from
SOW_DIRwhen set. An explicit-Csuppresses the current-directory candidate, but not theSOW_DIRfallback. - If no Workspace is found, exit
2.
--workdir changes only the discovery start directory. It does not change the process working
directory, so relative positional paths still resolve against the actual current directory.
sow create never performs Workspace discovery.
Repository selection
Commands that require one Repository select it in this order:
- explicit
-r/--repo NAME; - the Repository containing the discovery start directory;
- the only Repository in the Workspace;
- otherwise fail with exit
2and list the candidates.
repo new and repo rm take NAME positionally and do not accept -r. sow where searches all
Repositories by default; -r narrows its scope. Publication targets select their configured
Repository, so publish TARGET and gc TARGET do not accept an additional Repository selection.
Dist selection
add, rm, and ls require a concrete Dist set and select it in this order:
- repeated
-d/--dist NAMEvalues; - the Dist containing the discovery start directory;
- the only Dist in the selected Repository;
- otherwise fail with exit
2and list the candidates.
Other commands deliberately differ:
build,check, andstatusdefault to all Dists when-dis absent;showsearches the whole selected Repository unless-dnarrows it;wheresearches all matching Dists across the Workspace unless-r/-dnarrow it;changesis Repository-wide and rejects-d.
Locking
Write commands other than init accept -T/--timeout DUR and -N/--no-wait. init takes the
Workspace lock and waits without a CLI timeout override. Other locks are Repository-scoped except
for repo new and repo rm, which also use the Workspace lock. --timeout 0 waits indefinitely;
a positive timeout uses Go duration syntax such as 500ms, 30s, or 5m. --no-wait fails
immediately. A positive timeout and --no-wait are mutually exclusive. Lock acquisition failure
exits 4.
Read-only commands take no write lock. status still reports whether a writer holds the Repository
lock.
Parallelism
-j/--jobs N is available only where SOW parses packages, hashes bytes, renders indexes, or verifies
state: create, add, rm, build, check, and repo migrate. It defaults to the logical CPU
count and must be at least 1.
JSON output
Commands with --json emit one versioned envelope on stdout; diagnostics remain on stderr:
ok is false for every non-zero exit. A partial batch still returns its committed and failed items.
See JSON Output for complete result shapes.
Without --json, every Managed command has a stable human-readable renderer. Use it for
interactive work, not as a machine protocol. --json always selects the standard envelope and is
the only supported interface for scripts that need structured fields.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success or idempotent no-op |
1 |
Runtime I/O, parser, renderer, signing, or transport error |
2 |
Usage, Workspace discovery, or configuration error |
3 |
Partial batch success |
4 |
Write lock unavailable |
5 |
Integrity/recovery failure, or check ruling the tree not deliverable |
6 |
Expected rejection: conflict, protected object, no match, or incompatible architecture |
See Exit Codes for command-specific triggers.
1 - sow create
sow create turns a directory that already contains .rpm and .deb files into a flat repository
by writing indexes next to the packages. It is the whole of Plain mode: no sow.yml, no SQLite, no
Workspace discovery. This page covers the one-pass scan contract, the --pigsty completion gate,
and RPM signing with --sign-with.
Synopsis
DIR defaults to the current directory.
Description
create reads the top-level regular files in DIR and renders the index formats implied by what it
finds: repodata/ when RPMs are present, Packages and Packages.gz when DEBs are present, both
when the directory is mixed. All architectures come from the package headers — Plain mode has no
architecture flag and no permit list.
Flat metadata only ever references packages in the same directory. RPM location is the bare
basename and DEB Filename is ./<basename>, so both remain relative whether the directory is
exposed as a file:// source or an HTTP root.
By default create does not delete, move, rename, re-sign or rewrite a single package byte. It only
replaces index paths it owns; unknown files are left alone.
Options
| Flag | Description | Default |
|---|---|---|
-j, --jobs N |
Parallel workers for the single package hash/parse pass | logical CPU count |
--pigsty |
Enable Pigsty compatibility cleanup and completion marker | off |
-S, --sign-with KEY |
Sign unsigned RPMs with a 16/40/64-hex GPG key ID | off |
--overwrite |
Re-sign every RPM; requires --sign-with |
off |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
-h, --help |
Show help | — |
Scan rules
- Only top-level regular files ending in
.rpmor.debare considered. - No recursion, no symlink following, no Workspace config.
- Every valid version enters the index. Two files claiming the same logical coordinate with different content is a hard failure.
- A directory with no supported package is rejected in default mode.
--pigstyaccepts an empty authoritative set so an interrupted all-package cleanup can converge and write its marker.
Package I/O and final validation
For the normal unsigned path, each selected package has exactly one full content pass. A worker opens
it, computes SHA-256 once, parses its header/control metadata, and retains the complete parsed result.
RPM XML and DEB Packages are rendered from that retained result; neither rendering nor generated
metadata validation reopens package payloads. --jobs parallelizes this pass while canonical result
ordering keeps output bytes independent of worker scheduling.
Immediately before publication, create relists the top-level package set and compares file identity,
type/mode, size, and mtime with the post-scan snapshot. This is a cheap stat check, not a second hash.
A changed set or stat returns integrity error 5 before any staged output is published. Deliberately
preserving inode, size, and mtime while modifying bytes is outside the local cooperative-writer
contract.
Explicit RPM signing is an exception: copying, signing, signature verification, and parsing the final signed RPM necessarily add reads for packages that are modified.
Deterministic output and idempotence
The rendered metadata is byte-stable for a given input set: gzip output is deterministic, repomd.xml
carries <revision>0</revision> and timestamp 0. Running create twice on an unchanged directory
rewrites nothing and reports noop=true:
The repo_complete gate
Default mode never creates repo_complete. If the marker already exists, create refuses to write
indexes rather than leaving a stale marker claiming a build that no longer matches:
Either re-run with --pigsty (which withdraws and republishes the marker in its documented order)
or remove the marker yourself.
–pigsty
--pigsty enables three coupled compatibility actions in one invocation. Their publication order
is marker-gated, but the operation is rebuilt on retry rather than recovered from a journal:
- Delete DEB packages whose parsed architecture is
i386. RPMs are not removed merely for carrying ani386/i486/i586/i686architecture. - Delete RPM/DEB whose binary package name is exactly
patroniand whose upstream version is exactly3.0.4. RPM comparesVERSION, ignoring epoch and release; DEB strips epoch and Debian revision first.3.0.4+foois not a match. - After all indexes render successfully, write
repo_complete: the SHA-256 of every remaining top-level RPM/DEB, sorted by basename byte order, formatted<sha256><two spaces><basename>.
Cleanup only touches top-level regular package files that parsed successfully and matched a rule. Directories and unknown files are never removed by glob.
The publication order matters for callers that gate on the marker: the existing repo_complete is
withdrawn before indexes switch, matched packages are deleted only after replacement metadata is
installed, and the new marker is written last. A caller must treat a missing marker as incomplete.
Treat a missing repo_complete as “build in progress”. That is the contract --pigsty is designed
around.
Signing RPMs
-S/--sign-with KEY is the explicit authorization to modify RPM bytes. KEY is exactly 16, 40 or 64
hexadecimal characters; an 0x prefix is not accepted. SOW normalizes it to
uppercase and passes it to the environment’s rpm --addsign through the _gpg_name macro. The
private key, passphrase, GPG home, pinentry and any extra RPM macros come from your environment —
SOW never receives, persists or echoes a secret.
- Default: only RPMs with no parseable embedded OpenPGP signature are signed. Anything already signed keeps its bytes.
--overwriterequires--sign-withand switches torpm --resignover every retained RPM.- Signing happens on a private same-filesystem stage copy. Each result is re-parsed to confirm the embedded signature, the signature-neutral digest and NEVRA are unchanged, and rpm-md is generated from the final complete bytes.
- At least one top-level RPM must remain after
--pigstycleanup, andrpmmust be onPATH.
Locking, staging and overwrite rebuild
create takes a write lock on the target directory and honors --timeout/--no-wait. All metadata
is written to a private stage and validated before publication begins. The lock coordinates SOW
writers on the local machine; arbitrary external package mutation is unsupported.
Plain create does not create a durable operation journal, rollback pre-images, or recovery trash.
Publication consists of several single-file renames, so a crash may leave a partially replaced set of
derived files. Re-run sow create with the intended current options: it discards reserved stale Plain
temporary state and rebuilds all indexes from the packages that currently exist. recovered is
always false; a rerun is a fresh overwrite build, not replay.
A flat directory has no whole-repository generation pointer, and RPM plus DEB entry points cannot be
swapped in one POSIX rename. Plain therefore does not promise cross-file instantaneous atomicity. Use
repo_complete as the --pigsty gate, or use Managed mode when transactional recovery is required.
Examples
Index a mixed directory:
Machine-readable result:
Replace a Pigsty plain build with eight workers:
Failure envelope:
Exit codes
| Code | Trigger |
|---|---|
0 |
Indexes written, or unchanged input produced a no-op |
1 |
Directory unreadable or missing, package parse failure, renderer failure, signing tool failure |
2 |
Usage error — --overwrite without --sign-with, malformed key, --no-wait with a non-zero --timeout |
4 |
Directory write lock held and --no-wait given or --timeout expired |
5 |
Input set/stat changed before publication, or a controlled output path failed an integrity check |
6 |
No supported package found, repo_complete gate hit, --sign-with on a DEB-only directory, coordinate conflict |
See also
- Plain Flat Repositories — the design behind
create - Quick Start — five-minute flat repository walkthrough
- Repository Layout — what the flat tree looks like
- Sign Your Repository — generating and using a signing key
2 - sow init
sow init creates the root sow.yml and the private .sow/ state directory that make a directory a
Workspace. It is also the convergence command for a config you wrote by hand: if sow.yml already
declares Repositories and Dists, init materializes the ones that don’t exist yet and leaves the
finished ones alone.
Synopsis
DIR defaults to the current directory. init takes no -C/--workdir — the positional argument
already names the target unambiguously.
Description
A fresh init writes a minimal config and the private state directory:
.sow/ holds workspace.lock, the workspace-ops/ durable file journal used by Workspace lifecycle
commands, repo-locks/, and later one SQLite database per Repository. It is mode 0700 and must
never be served over HTTP.
Options
| Flag | Description | Default |
|---|---|---|
--json |
Emit the versioned JSON envelope | false |
-h, --help |
Show help | — |
Idempotence rules
init is designed to be safe to run repeatedly, in a provisioning script or by hand:
-
It writes
schema: sow/v3and the defaultarchitectures: [x86_64, aarch64]when creating a new config. -
It never creates a Repository on its own. Use
sow repo new, or declare one insow.ymlfirst. -
It never overwrites an existing
sow.yml. A repeat run reports the current state and lists what it found: -
A non-empty directory can be initialized, but the run fails if an existing file collides with a SOW reserved path.
Converging a declared configuration
If sow.yml already describes Repositories and Dists, init creates the missing directory trees,
SQLite databases and empty indexes for them. Already-initialized objects are skipped, so the counters
tell you exactly what this run did.
Every Dist created this way has a protocol-complete empty surface: an RPM Dist gets an empty
repodata/ per architecture view, and a DEB Dist gets empty Packages/Packages.gz with
by-hash plus a Release.
Running it a second time changes nothing:
Locking and recovery
Workspace lifecycle commands — init, repo new, repo rm — run before the target Repository
database exists or after it is deleted, so they use .sow/workspace.lock plus the durable file
journal in .sow/workspace-ops/ rather than a SQLite Operation Journal. An interrupted init is
completed or rolled back by the next Workspace lifecycle command.
Examples
Bootstrap a Workspace and add Repositories by hand:
Initialize a directory other than the current one:
Provision from a config file under version control:
Exit codes
| Code | Trigger |
|---|---|
0 |
Workspace created, or already converged (no-op) |
1 |
Runtime I/O error writing the config or state directory |
2 |
Usage error, or an existing sow.yml that fails to parse or validate |
3 |
Partial success — some declared Repositories/Dists were committed and at least one failed |
5 |
The Workspace journal could not be recovered to a terminal state |
6 |
An existing file collides with a SOW reserved path |
See also
- First Workspace — the ten-minute guided version
- Managed Workspaces — the three-layer model
- sow.yml Reference — every configuration key
- sow repo and sow dist
- Repository Layout — what
.sow/contains
3 - sow config
sow config has two read-only subcommands. config check is the full preflight over sow.yml —
run it after every hand edit and in CI. config show prints the configuration SOW actually computed,
which is where you confirm that defaults, inherited architectures and normalized aliases resolved the
way you expected.
Neither subcommand creates directories, touches a database, or corrects your file.
Synopsis
sow help config lists both.
sow config check
Parses and validates the complete sow.yml: schema version, names, path collisions, the architecture
permit list, Dist formats, membership policy and signing key references. It reports the Workspace it
resolved and how much it validated.
Options
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
--json |
Emit the versioned JSON envelope | false |
-h, --help |
Show help | — |
Strict field rejection
Unknown keys are errors, not warnings. A typo can’t silently disable a policy:
The schema version is pinned:
The only valid value is schema: sow/v3. Do not change the schema string as a
way to bypass a validation error.
check also verifies that every declared signing key reference resolves and is usable for signing —
without ever printing key material. If you remove an architecture from the permit list while a Dist
config, Membership or Built Generation still uses it, config check rejects the configuration.
sow config show
Prints the effective configuration as YAML for the currently selected scope.
Compare that with the file on disk, which carries only what you wrote:
show filled in protected: false, the inherited per-Dist architectures, limit: 0 and an empty
exclude list. Architectures are always printed as canonical families (x86_64, aarch64), never
as ecosystem aliases — amd64 and arm64 are the DEB spellings of the same two families.
Options
| Flag | Description | Default |
|---|---|---|
--all |
Expand defaults and normalized architectures across the whole Workspace | off |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Select a distribution; repeatable | selection rules |
--json |
Emit the versioned JSON envelope | false |
-h, --help |
Show help | — |
Scope projection with -r and -d
-r and -d narrow the output to the selected objects. This is the fast way to answer “what policy
is actually in effect for this one Dist”:
--all goes the other way: it expands the entire Workspace regardless of where you are standing.
Secrets are never printed
Key material and passphrases never appear in config show, in JSON, in the operation log, or in
error text. Only the reference (file://…, env://…, agent://…) and fingerprint are shown.
Examples
Validate before a build in CI:
Diff effective policy across two Dists:
Exit codes
| Code | Trigger |
|---|---|
0 |
Configuration valid, or output printed |
1 |
Runtime I/O error reading the config file |
2 |
Usage error, Workspace not found, unknown field, wrong schema, or any validation failure |
6 |
A named Repository or Dist does not exist |
config check reports validation failures as exit 2, not 6: an invalid sow.yml is a
configuration error, not a rejected operation.
See also
- sow.yml Reference — every key, with a complete example file
- Membership Policy — how
excludeandlimitare evaluated - Signing Model — key reference grammar and the two trust chains
- sow init — converging a hand-written config
- sow check — the runtime counterpart that verifies bytes on disk
4 - sow repo
A Repository owns one pool/, one dists/, one SQLite database and one private state directory. It
is the boundary of locking, transaction recovery, Generation numbering and Changesets — nothing is
deduplicated across Repositories and no cross-Repository commit is atomic. sow repo manages that
boundary.
Synopsis
Naming
A Repository name must match [a-z0-9][a-z0-9._-]* and may not be ., .., .sow, pool,
dists, or collide with a Workspace reserved file.
You cannot choose the path. A Repository always lives at <workspace>/<NAME>/.
sow repo ls
Read-only listing of every Repository in the Workspace.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
--json |
Emit the versioned JSON envelope | false |
STATUS is one of clean, dirty, recovering or error. See
Transactions & Recovery for what each one implies for clients.
sow repo new
Atomically updates sow.yml, then creates <workspace>/<NAME>/{pool,dists}, the SQLite database and
the private state directory. A new Repository is Generation 0 and clean.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
repo new takes the Workspace lock, not a Repository lock — the Repository database does not exist
yet. It does not accept -r; the positional argument already names the target.
Running it again on an existing Repository is a converging no-op that reports the current state, so it is safe in a provisioning script.
sow repo show
Read-only detail for one Repository. With NAME omitted, the usual
Repository selection rules apply.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select the repository when NAME is omitted |
selection rules |
--json |
Emit the versioned JSON envelope | false |
If you give both NAME and -r, they must agree; disagreement fails before any state is read:
sow repo migrate
This is a specialized maintenance command, not part of a fresh 0.4 Managed workflow. A Repository created by SOW 0.4 already has the current single-payload layout and schema.
After upgrading an existing v0.3 Workspace, however, migration is mandatory: stop all Workspace writers, take a backup, and run the command once for every configured Repository before ordinary reads or writes.
The 0.4 transition installs schema v11 and v12. It recomputes Repository status from every Dist, repairs publication and Generation signer projections without guessing a missing historical signer, removes stale abandoned-object evidence, and backfills revision 1 of the append-only publication-target binding ledger. An unrecorded v0.3 historical signer remains explicitly unverified; it cannot reach the current head or become a retained trust assertion.
The completed schema transition is one-way. Do not reopen the database with SOW 0.3, and do not edit
PRAGMA user_version. --abort applies only to a diagnosed pre-commit layout-maintenance attempt;
it does not undo a completed schema migration. Outside an upgrade or an explicit SOW diagnostic,
do not run migration speculatively.
| Flag | Description | Default |
|---|---|---|
-j, --jobs N |
Parallel verification/render workers | logical CPUs |
--abort |
Abandon a maintenance attempt before its commit decision | false |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select the repository when NAME is omitted |
selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
sow repo rm
Removes a Repository: its sow.yml entry, database, pool/, dists/ and private state. It never
follows symlinks and never steps outside the fixed Repository path.
Without -f, only an empty Repository — no Dists, no Memberships, no Package Objects — can be
removed:
| Flag | Description | Default |
|---|---|---|
-f, --force |
Remove a non-empty unprotected repository | false |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
What -f actually downgrades
-f only relaxes the emptiness precondition. It does not bypass path safety, symlink refusal, or
the protected gate.
protected
protected: true in sow.yml blocks Repository deletion outright, -f included:
To remove a protected Repository you must edit sow.yml, pass
sow config check, and try again. There is no --yes and no
temporary override.
protected scopes to Repository deletion only. Package-level work on a protected Repository is
unaffected — add, rm, build, and even dist rm, all continue to work:
Examples
Create the Repositories for a two-tier layout:
Fail fast in a cron job rather than queue behind another writer:
Audit every Repository in one line each:
Exit codes
| Code | Trigger |
|---|---|
0 |
Listed, created, shown, migrated, abandoned a pre-commit transition, or removed; or repo new converged an existing Repository |
1 |
Runtime I/O error creating or removing the tree |
2 |
Usage error, Workspace not found, or an ambiguous Repository selection |
4 |
Workspace lock held and --no-wait given or --timeout expired |
5 |
Integrity or recovery error in the Workspace journal |
6 |
Invalid name, unknown Repository, non-empty without -f, protected, or NAME conflicting with -r |
See also
- sow dist — the layer below
- Managed Workspaces — the three-layer model and discovery rules
- Transactions & Recovery — lock scopes and the
recoveringstate - sow.yml Reference —
protectedand per-Repository signing - Repository Layout — the fixed directory structure
5 - sow dist
A Dist is a named set of packages in exactly one format (rpm or deb) inside one Repository. It is
what a client points at. A Repository can hold RPM and DEB Dists side by side; they share one pool/
but render into completely separate dists/ subtrees.
Synopsis
Naming
Dist names follow the same rule as Repository names: [a-z0-9][a-z0-9._-]*, excluding ., ..,
.sow, pool and dists.
To SOW the name is an opaque string. el9, trixie, el9-beta, customer-acme, 2026-07-31 are
all just names — beta channels, per-customer views and snapshots are naming conventions you impose,
not features SOW models.
sow dist ls
Read-only flat listing of the selected Repository’s Dists.
DESIRED and BUILT are membership counts. When they diverge, DIRTY_REASONS says why:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
--json |
Emit the versioned JSON envelope | false |
Architectures print as canonical families. The JSON output carries both spellings, which is how you
confirm that a DEB Dist renders binary-amd64 and binary-arm64:
sow dist new
Creates an ordinary, still-editable Dist. The only business argument is --format.
| Flag | Description | Default |
|---|---|---|
--format FORMAT |
Required; rpm or deb |
— |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
--format is mandatory and closed:
There is no --arch. Architectures are inherited from the Workspace permit list; an advanced user
narrows them per Dist by editing sow.yml. Policy (limit, exclude) is likewise configured in
sow.yml, never re-modelled on the command line.
Re-running dist new with the same name and the same format converges and reports the current state.
A name collision with a different format is rejected:
The three-way transaction
dist new is committed across three places at once: the sow.yml entry, the Repository database,
and the on-disk tree. It goes through the SQLite Operation Journal (the Repository database already
exists at this point, unlike repo new) and produces a new Built Generation with empty indexes.
That means a fresh Dist has a protocol-complete empty surface. An RPM Dist gets an empty
repodata/ under every architecture view; a DEB Dist gets empty Packages, Packages.gz,
the by-hash/SHA256/ entries and Release, plus InRelease/Release.gpg when signing is
configured.
sow dist show
Read-only detail for one Dist.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
--json |
Emit the versioned JSON envelope | false |
The JSON form additionally exposes effective_config_sha256, the digest of the resolved Dist
configuration. That digest is what makes a Dist dirty when you change limit, exclude or a signing
key — the config identity changed, so the Built Generation no longer matches Desired.
sow dist rm
Removes a Dist’s Membership and derived indexes.
| Flag | Description | Default |
|---|---|---|
-f, --force |
Remove membership and indexes but retain pool packages | false |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
Dist removal does not delete Pool bytes
Removing a Dist never deletes a package from pool/. The whole Dist directory is moved into the
recovery area and removed atomically; the pool is untouched:
Orphaned pool objects remain until sow gc proves they are unreachable from every safety
root: current, retained, recovery, publication, and any active maintenance operation.
A Repository’s protected: true blocks Repository deletion only; normal Dist maintenance on a
protected Repository continues to work.
Examples
Give one Repository an RPM and a DEB face:
Add a beta channel with its own retention policy — create it, then set the policy in sow.yml and
converge:
Which Dists are behind their Desired state:
Exit codes
| Code | Trigger |
|---|---|
0 |
Listed, created, shown or removed; or dist new converged an existing Dist |
1 |
Runtime I/O or renderer error creating the empty indexes |
2 |
Usage error — missing or invalid --format, Workspace not found, ambiguous Repository |
4 |
Repository lock held and --no-wait given or --timeout expired |
5 |
Integrity or recovery error in the Operation Journal |
6 |
Invalid name, unknown Dist, format conflict with an existing name, non-empty without -f |
See also
- sow repo — the layer above
- Pool & Architecture Views — how one Dist becomes several client-visible views
- Membership Policy — configuring
limitandexclude - sow build — converging after a policy edit
- Repository Layout — the RPM and DEB
dists/structures
6 - sow add
sow add is the main write path. It parses the packages you point at, derives their format and
architecture from the package headers, applies the Dist’s membership policy, and — unless you pass
--skip — rebuilds every affected index before it returns. When the command exits 0, clients can
already see the new packages.
Synopsis
Options
| Flag | Description | Default |
|---|---|---|
-R, --recursive |
Descend into subdirectories of a PATH directory |
off (top level only) |
--skip |
Update Desired state only; do not build | off |
-j, --jobs N |
Parallel workers for parsing, hashing and rendering | logical CPU count |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Select a distribution; repeatable | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
Inputs and targets
PATH can be a file or a directory. A directory is scanned top level only unless you pass -R.
You must end up with exactly one Repository and at least one target Dist — see the selection rules. Mixed RPM/DEB batches are fine: each package is only considered for target Dists of its own format. A package with no compatible target fails.
SOW never infers the target from a manifest, a directory name, or the host OS.
The summary line reports the Operation ID, per-item counts, the membership delta, the new Desired
Revision, the Built Generation and whether the Repository is left dirty. Then one item line per
input, in stable order.
Item statuses
Each item line carries an overall status plus a per-Dist verdict in dists=.
| Status | Meaning |
|---|---|
accepted |
New Package Object created and at least one Membership added |
reused |
The content already exists in this Repository; only Membership references may change |
excluded |
Policy removed it from every target Dist — see the dists= field for excluded vs limited |
failed |
The package was rejected; the error= field says why |
reused is content idempotence: adding the same file twice never creates a second object or a
duplicate Membership. A default repeated add also converges the selected Dist. If it is already
current, the Generation stays put; if an earlier --skip or configuration change left it dirty,
the reused input triggers the missing build and may advance the Generation:
The same object added to a second Dist is also reused — the pool keeps one copy and gains a second
Membership. Use --skip again when the intent is to retain a dirty batch instead of converging it.
Architecture is read, never guessed
add reads format and native architecture from the package header, then checks the Workspace permit
list. An architecture outside the list fails the package and tells you exactly what to edit:
It does not create a directory and does not modify sow.yml.
RPM noarch and DEB all are architecture-neutral. They create one Package Object and one
Membership, and render into every effective architecture view of the target Dist. They do not
spread to Dists you did not select with -d.
Policy: exclude and limit
After merging into the target Memberships, SOW re-evaluates exclude and then limit over the
complete Dist candidate set. A package removed by policy is reported, not treated as a parse failure.
Here trixielim has exclude: [{kind: [dbgsym]}] and limit: 1. The dbgsym package was excluded by
rule; libpq5 18.2-1 lost to 18.3-1 under the version limit and is reported as limited. Both
appear as excluded in the top-level status, and the dists= field distinguishes them.
Limit groups by (binary name, native architecture), so 18.3-1:amd64 and 18.3-1:arm64 both
survive a limit: 1. A package can be accepted by one Dist and skipped by another in the same run.
exclude and limit remove real Desired Memberships. Relaxing the policy later does not resurrect
them — leftover bytes in pool/ are not a candidate set. Re-run sow add.
Partial batches
Valid, conflict-free packages are committed even when siblings fail. Failed inputs stay where they
are, each with its own error, and the command exits 3:
If nothing is accepted, the whole operation is rejected with exit 6 and the Repository is
unchanged:
There is no rejected/quarantine directory.
–skip
--skip stops after the Desired state is committed. The public pool/ and dists/ bytes do not
change, the Built Generation stays where it was, and the Repository becomes dirty. New package bytes
are durably held in a private pending store until the next build publishes them.
pending=4/2326 is four objects totalling 2326 bytes waiting in the private store. They never appear
in sow changes — only a successful build promotes them into the
deliverable tree.
Use --skip for bulk imports, then converge once:
Processing order
For the record, one add executes in this order:
- Take the Repository write lock and recover any unfinished Operation.
- Commit a
plannedOperation in SQLite. - Parse inputs read-only; compute logical coordinates and input SHA-256 (plus, for RPM, the signature-neutral payload digest).
- Check the architecture permit list and look up existing coordinates.
- For genuinely new coordinates only, run optional RPM signing on a stage copy and compute the final SHA-256, then verify content and path uniqueness.
- Merge target Memberships, then apply
excludeandlimitover the complete Dist set. - Commit the Desired state; new bytes go to the private pending content store.
- Unless
--skip, publish still-needed pending objects intopool/and render indexes — each Dist is built at most once per command.
Input files are never modified, moved or deleted, in any mode.
RPM signing modes
Managed RPM package signing is configured in sow.yml under signing.rpm.packages.mode; there is no
command-line override.
| Mode | Behavior |
|---|---|
never |
Keep the input bytes exactly |
fill |
Sign when unsigned or when the signature is not trusted; keep bytes when a trusted_keys signature verifies. Default when a key is configured |
always |
Ensure the final package is validly signed by the configured key; re-sign a stage copy otherwise |
Without a configured key only never is available.
Because a signature embeds non-deterministic fields, SOW cannot re-sign and then compare final
hashes. Retry idempotence works on the coordinate instead: identical input bytes are reused
directly; an identical RPM signature-neutral digest is reused when the existing object satisfies the
current policy. A different payload digest, or an existing object that no longer satisfies the
policy, is a hard conflict — add will not silently re-sign a coordinate in place.
Exit codes
| Code | Trigger |
|---|---|
0 |
Every input accepted or reused; indexes rebuilt (or skipped with --skip) |
1 |
Runtime I/O, parser, renderer or signing failure |
2 |
Usage error, Workspace not found, or ambiguous Repository/Dist selection |
3 |
Partial batch — at least one item committed and at least one failed |
4 |
Repository lock held and --no-wait given or --timeout expired |
5 |
Integrity or recovery error, including a build that failed after applied |
6 |
Nothing accepted — unsupported architecture, no compatible target Dist, or a coordinate conflict |
See also
- sow rm — the inverse operation
- sow build — converging after
--skip - Membership Policy —
excluderules andlimitsemantics in full - Signing Model — the two trust chains and key references
- Package References — the coordinate grammar printed on each item line
7 - sow rm
sow rm takes packages out of the Desired Membership of the Dists you select and, by default,
rebuilds the affected indexes immediately. It does not delete bytes from pool/ — membership and
content are separate concepts, and reclamation is the separate conservative sow gc operation.
Synopsis
Options
| Flag | Description | Default |
|---|---|---|
-c, --check |
Preview only; compute and print the plan without writing anything | off |
--skip |
Update Desired state only; do not build | off |
-j, --jobs N |
Parallel workers | logical CPU count |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Select a distribution; repeatable | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
--check and --skip are mutually exclusive:
Package references
PACKAGE accepts five forms. The full grammar and disambiguation rules are on
Package References; the short version:
| Form | Example |
|---|---|
| Content hash | sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab |
| RPM coordinate | rpm:epel-release-0:7-5.noarch |
| DEB coordinate | deb:libpq5=18.3-1:amd64 |
| Full filename | epel-release-7-5.noarch.rpm |
| Bare binary name | epel-release |
A bare name means every version and native architecture of that name in the selected Dists — that
is what makes sow rm patroni a useful takedown command. An ambiguous short reference that is not a
bare name fails and lists the candidates instead of guessing.
sow ls prints exact sha256: references and canonical coordinates,
so you never have to assemble one by hand.
A reference matching nothing is a rejection, not a silent success:
There is no --allow-empty, no --all, no --yes and no --source-list.
Preview with –check
-c/--check computes exactly what would be removed, what policy would then decide, and which files
an immediate build would touch — and writes nothing at all.
Note both centos-release versions matched the bare name. The change lines are a real delivery
plan in payload → metadata → pointer → delete phase order. Use --json when another program needs
the corresponding removed[] and changes[] arrays.
Preview uses the same candidate-configuration and integrity preflight as the mutation. A preview that fails that guard is not evidence that the write would succeed.
--check deliberately does not take the write lock. Combining it with lock flags is a usage error,
so nobody can believe a preview is queueing behind a writer:
Default behavior: remove and rebuild
Without --check or --skip, rm commits the Desired change and rebuilds every affected Dist
before returning. Pool objects stay on disk.
The summary is followed by one change line per affected file (eight in this run). Add --json to
receive the same result as a stable standard envelope.
Removing the last member of a Dist is fine. SOW still renders a valid, signed-if-configured empty
index — an empty Packages with a verifiable InRelease, or empty per-architecture repodata/.
–skip
--skip commits the Desired change and marks the Repository dirty without touching the public tree.
The old Built Generation stays completely self-consistent for clients.
changes is empty because nothing was built. Run sow build to
converge.
Policy interaction
Removals are Desired-state edits, so policy is re-evaluated over the resulting candidate set — a
removal will never resurrect a package that limit previously pushed out. If you remove libpq5 18.3-1 from a limit: 1 Dist, 18.2-1 does not come back; add it again explicitly.
Examples
Safe takedown — preview first, then execute:
Remove one exact object from two Dists at once:
Batch several removals, then rebuild once:
Feed the preview plan to another tool:
Exit codes
| Code | Trigger |
|---|---|
0 |
Memberships removed and rebuilt, or a --check preview printed |
1 |
Runtime I/O or renderer failure |
2 |
Usage error — --check with --skip, --check with lock flags, ambiguous selection, Workspace not found |
3 |
Partial batch — at least one reference removed and at least one failed |
4 |
Repository lock held and --no-wait given or --timeout expired |
5 |
Integrity or recovery error |
6 |
A reference matched nothing, or an ambiguous non-bare reference |
See also
- Package References — the full reference grammar
sow ls,show, andwhere— finding the exact reference to remove- sow add — the inverse operation
- sow build — converging after
--skip - Membership Policy — why removed members do not come back
sow retainandsow gc— when bytes can be reclaimed
8 - sow ls
sow ls is a read-only query over Package Objects and Dist Membership. It shows what each selected
Dist should contain and whether that membership is present in the current Built Generation.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
-d, --dist NAME |
Select a Dist; repeatable | selection rules |
--json |
Emit the sow.cli/v1 envelope |
false |
There is no --pool, --match, or output-format flag.
Output
| Column | Meaning |
|---|---|
SHA256 |
Immutable content identity; valid input to show and rm |
COORDINATE |
Canonical rpm: or deb: package reference |
DISTS |
Desired Membership across the selected scope |
BUILT_DISTS |
Membership present in the current Built Generation |
POOL_PATH |
Repository-relative immutable payload path |
The first line reports dirty=true when Desired and Built state differ. An empty BUILT_DISTS
field means the package is desired but clients cannot see it yet. Run sow build
to converge.
An object shared by several selected Dists appears once, with comma-separated membership values. An empty Dist has a header and no package rows; that is a successful result.
Selection
ls requires an unambiguous Dist set. In a multi-Dist Repository, pass one or more -d values or
run from inside <repo>/dists/<dist>/.
The command takes no write lock and does not hash package files. --json returns the same rows in
result.packages.
Examples
List exact references for all objects not yet built:
List pool paths in deterministic order:
Exit codes
| Code | Trigger |
|---|---|
0 |
Membership printed, including an empty list |
1 |
Runtime I/O failure |
2 |
Usage error, Workspace not found, or implicit Repository/Dist selection is ambiguous |
5 |
Repository state database unreadable or inconsistent |
6 |
Explicit Repository or Dist is not configured |
See also
sow show— inspect one listed objectsow where— locate an object across the Workspacesow rm— remove a listed reference from Desired Membership- Package References — accepted identity forms
9 - sow show
sow show resolves one package reference in the selected Repository and prints the complete Package
Object. It is read-only and takes no write lock.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
-d, --dist NAME |
Narrow candidates to these Dists; repeatable | Repository scope |
--json |
Wrap the result in the sow.cli/v1 envelope |
false |
Package reference
PACKAGE accepts a sha256:<hex> content identity, canonical rpm:<NEVRA> or
deb:<name>=<version>:<arch> coordinate, full package filename, or bare binary name. See
Package References for the exact grammar.
A bare name must resolve to exactly one Package Object in the selected scope. Unlike sow rm foo,
which removes every matching version from Desired Membership, sow show foo refuses ambiguity and
prints the candidates:
Copy an exact coordinate or SHA-256 from the error or from sow ls and retry.
Output
Without --json, show prints the identity, storage path, and Desired/Built locations in a compact
human-readable form:
Adding --json returns the complete Package Object under result in the standard envelope,
including the normalized fields below.
| Field | Meaning |
|---|---|
canonical_arch |
x86_64, aarch64, or neutral for RPM noarch / DEB all |
kind |
Policy class: main, debuginfo, debugsource, llvmjit, dbgsym, or dbg |
source |
Normalized source-package name |
payload_sha256 |
RPM signature-neutral digest used for re-signing idempotence |
signature_key |
Embedded package-signature key ID, when present |
storage |
pending before build; pool once published into the repository tree |
dists / built_dists |
Desired and current Built Membership |
-d narrows candidate resolution; it does not alter package identity.
Exit codes
| Code | Trigger |
|---|---|
0 |
One Package Object printed |
1 |
Runtime I/O failure |
2 |
Usage error, Workspace not found, or implicit Repository selection is ambiguous |
5 |
Repository state database unreadable or inconsistent |
6 |
Explicit scope is not configured, or the reference matched nothing/several objects |
See also
sow ls— obtain exact identities from Dist Membershipsow where— search across Repositoriessow rm— remove matching Desired Membership- JSON Output — complete result schema
10 - sow where
sow where answers which Dists in the Workspace still carry one Package Object. It is read-only,
searches every Repository by default, and takes no write lock.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Restrict the search to one Repository | all Repositories |
-d, --dist NAME |
Restrict the search to named Dists; repeatable | all Dists |
--json |
Emit the sow.cli/v1 envelope |
false |
Reference resolution
PACKAGE uses the same grammar as sow show: SHA-256 identity, canonical
RPM/DEB coordinate, full filename, or bare name.
Resolution happens across the complete selected scope. A bare name must identify one Package Object;
different objects with the same binary name are ambiguous even when they live in different
Repositories. Use -r/-d, or supply an exact coordinate or SHA-256.
Output
Without --json, where prints a summary followed by one line per location:
Each location reports both Desired dists and current built_dists. This makes the command useful
for answering whether a removed or superseded build is still client-visible anywhere.
With --json, the same object appears under result. A missing reference is an expected rejection,
not an empty success:
Example
List every location still serving an exact build:
Exit codes
| Code | Trigger |
|---|---|
0 |
One resolved Package Object and its locations printed |
1 |
Runtime I/O failure |
2 |
Usage error or Workspace not found |
5 |
A Repository state database is unreadable or inconsistent |
6 |
Explicit Repository/Dist is not configured, or the reference matched nothing/was ambiguous |
See also
sow show— inspect the resolved Package Objectsow ls— list one Dist set- Package References — exact grammar and ambiguity rules
11 - sow status
sow status is the cheap Repository health query. It reads state but does not hash files, verify
signatures, recover Operations, build metadata, or take the write lock.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
-d, --dist NAME |
Restrict status to these Dists; repeatable | all Dists |
--json |
Emit the sow.cli/v1 envelope |
false |
Repository states
Every Repository tracks a Desired Revision in SQLite and the Built Generation represented by its
public dists/ tree.
| State | Meaning | Public view |
|---|---|---|
clean |
Desired state matches Built state | current, complete Generation |
dirty |
Desired state is ahead, commonly after --skip or a config change |
previous complete Generation |
recovering |
A non-terminal Operation must be recovered by the next write command | last completed protocol pointer |
error |
Automatic recovery cannot choose safely | last completed view; no overwrite attempted |
Dirty never means a half-written repository. Readers see either the old complete view or the new complete view because protocol pointers are switched last.
Output
The human line reports:
- Repository state and
ready_to_copy; - Desired Revision and current Built Generation;
- affected Dists;
- pending object count and bytes;
- write-lock state.
The JSON result additionally includes dirty_reasons and the most recent Operation:
ready_to_copy=false is a hard warning. true is only a cheap state result, not a byte-level proof;
run sow check before delivery.
Read-only contract
status never migrates or repairs state. If the Repository database cannot be read safely,
the command exits 5; run the maintenance command named by the diagnostic before retrying. In
particular, back up and run sow repo migrate for every
v0.3 Repository before using the 0.4 read surface.
Exit behavior
status returns 0 for every readable state, including dirty, recovering, and error. Scripts
should inspect the structured state rather than treating those conditions as command failures.
| Code | Trigger |
|---|---|
0 |
Repository state is readable |
1 |
Runtime I/O failure |
2 |
Usage error, Workspace not found, or implicit Repository selection is ambiguous |
5 |
State database unreadable or inconsistent |
6 |
Explicit Repository or Dist is not configured |
See also
sow build— converge a dirty Repositorysow check— use a full integrity/readiness gatesow log— inspect the recent Operation reported here- Transactions & Recovery — state transitions and pointer safety
12 - sow build
sow build is the explicit Desired-to-Built convergence command. It acquires the Repository write
lock, recovers any decidable unfinished Operation, renders and verifies a complete Generation, then
switches protocol pointers last.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-j, --jobs N |
Parallel workers; must be at least 1 |
logical CPU count |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
-d, --dist NAME |
Build named Dists; repeatable | all affected Dists |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Wrap the result in the sow.cli/v1 envelope |
false |
Without -d, SOW converges every affected Dist in the selected Repository. With -d, only those
Dists converge; unselected changes remain dirty.
Result
Without --json, build prints one human-readable summary:
Use --json for the command-specific object inside the standard envelope.
No-op builds
When membership, relevant policy, renderer settings, and signing configuration already match the
Built Generation, build is an idempotent no-op and does not increment the Generation:
Policy convergence
build re-evaluates the current exclude and limit policy. Tightening policy may remove Desired
Membership. Loosening policy does not reconstruct historical members from leftover pool bytes; run
sow add again for packages you want to restore.
Commit and recovery
SOW stages new metadata on the same filesystem, verifies it, then switches mutable protocol pointers last. RPM checksum-named metadata and APT by-hash keep old and new readers self-consistent.
Pending package promotion is a bounded, single-writer group commit. Each batch contains at most 512 objects or 1 GiB: SOW creates Pool links, persists every distinct target parent, then removes pending names and persists the shared pending directory. An interruption can leave a pending-only, exact dual-link, or Pool-only state, all recoverable from the journal; it cannot durably lose both names.
One Operation may cover several Dists. Each Dist always exposes a complete view; when build
returns, every included Dist belongs to the same Built Generation.
Before starting new work, build attempts forward recovery or safe rollback of a non-terminal
Operation. If journal, database, and filesystem evidence contradict each other, the Repository enters
error and build refuses to guess. There is no force-repair flag.
Progress events
Long builds append structured build_progress records to the Operation log. Each event
contains phase, completed, total, and jobs. Current phases are:
rendering;promoting_payload;publishing_dists;normalizing_public_tree;finalizing.
These events do not advance the Operation state and deliberately do not checkpoint SQLite
after every update. They are audit/observability records, not recovery decisions. Inspect
them with sow log OPERATION.
Metadata signing
Managed metadata signing is configured only in sow.yml; there is no command-line key override.
Changing a configured key reference or fingerprint makes affected Dists dirty, and the next build
re-signs their metadata.
- RPM: always writes
repodata/repomd.xml; writesrepomd.xml.ascwhen configured. - DEB: always writes
Release; writesInReleaseandRelease.gpgwhen configured.
Exit codes
| Code | Trigger |
|---|---|
0 |
Converged successfully or nothing to do |
1 |
Renderer, signing, or filesystem failure |
2 |
Usage error, Workspace not found, or implicit Repository selection is ambiguous |
4 |
Repository write lock unavailable |
5 |
Recovery cannot complete safely, or Repository is in error |
6 |
Explicit scope is not configured, or current configuration rejects existing state |
See also
sow status— determine whether convergence is neededsow check— verify the resulting treesow changes— inspect the resulting file delta- Transactions & Recovery — full commit protocol
13 - sow check
sow check is the deep read-only gate for a managed Repository. It hashes bytes, validates state,
reconstructs expected views, and verifies declared signatures. It never repairs, builds, recovers an
Operation, or takes the write lock.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-j, --jobs N |
Parallel verification workers; at least 1 |
logical CPU count |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
-d, --dist NAME |
Verify named Dists; repeatable | all Dists |
--json |
Emit the sow.cli/v1 envelope |
false |
Verification layers
In steady state, the checker reports nine ordered layers:
| Layer | Verification | checked counts |
|---|---|---|
config |
sow.yml parses and validates for the Repository |
configuration objects |
state |
SQLite quick_check, foreign keys, journal, and recovery evidence |
one state database |
public-modes |
File and directory permissions across the served tree | inspected paths |
retained |
Explicit retained records and frozen Generation manifests | retained records |
package-bytes |
SHA-256 of pool and private pending payloads | Package Objects |
desired-membership |
Membership rows resolve under current policy | memberships |
index |
Rendered indexes match the membership they claim | Dists |
signature |
Every declared metadata and package signature verifies | signatures |
generation-manifest |
Built Generation manifest matches files on disk | one manifest |
During a non-terminal Repository layout transition, check instead reports config, state,
public-modes, then a conditional layout-transition layer and stops. It returns not-ready until
the diagnosed repo migrate operation completes or is
aborted before commit.
Physical evidence and I/O contract
package-bytes never trusts a cached fingerprint as a substitute for authenticity. Each run hashes
every unique physical payload exactly once, using descriptor-bound evidence keyed by device, inode,
size, mtime, and ctime. Hard links to the same inode share that proof; retained Generations, final
manifest traversal, and changes reuse it without another payload scan. The checked column counts
logical objects, not the number of full-file streams.
A DEB or unsigned RPM needs one complete payload stream. A signed RPM uses at most one additional main-header-to-EOF stream to verify every signature packet against all candidate trust rings; the cost does not grow with Dist, retained Generation, or trust-ring count. A forged or concurrently replaced file invalidates the descriptor evidence and fails closed.
Dirty is not deliverable
A dirty Repository can have nine individually valid layers: the old Built Generation is intact and the new Desired state is valid. It still fails the delivery gate because the two do not match:
The exit code is 5. Run sow build and check again. Do not weaken a release
pipeline to accept this state.
Exit codes
| Code | Trigger |
|---|---|
0 |
Every layer passes and the Repository is ready to copy |
1 |
I/O failure during verification |
2 |
Usage error, Workspace not found, or implicit Repository selection is ambiguous |
5 |
A verification layer failed, or Repository is not deliverable |
6 |
Explicit Repository or Dist is not configured |
See also
sow status— cheap state querysow build— converge Desired and Built state- Exit Codes — why a dirty Repository maps to
5 - Observability & Audit — operating checks and audit together
14 - sow changes
sow changes compares Built Generations. It reports physical Repository-relative file changes; it
does not show unbuilt Desired changes and is not a remote transaction protocol.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
--json |
Emit the sow.cli/v1 envelope |
false |
The command is Repository-wide and rejects -d/--dist.
Output
Columns are operation, phase, Repository-relative path, size, and SHA-256.
| Field | Values |
|---|---|
| operation | add, update, delete |
| phase | payload, metadata, pointer, delete |
Phases describe how SOW constructed the local Generation. Do not replay individual rows into a live
remote tree. Use sow publish, or stage and atomically switch a complete
copy.
Base Generation
Without an argument, SOW compares the current Built Generation with its predecessor.
BASE_GENERATION is a decimal integer in the inclusive range 0..current. Base 0 produces the
complete delivery manifest for the current Generation, excluding private sow.yml and .sow/.
Using the current Generation as base produces an empty plan. A Repository never built also yields an
empty 0 -> 0 plan.
Dirty and recovery states
When Desired state is dirty, the header says dirty=true, but the plan still ends at the current
Built Generation. Private pending payloads are excluded because they are not deliverable yet.
When the Repository is recovering or error, changes refuses to emit a plan: pending file
actions must not be mistaken for a completed Generation.
Examples
Produce a complete manifest:
Filter one Dist by path after producing the Repository-level plan:
Exit codes
| Code | Trigger |
|---|---|
0 |
Plan printed, including an empty plan |
1 |
Runtime I/O failure |
2 |
Usage error, -d supplied, Workspace not found, or implicit Repository selection is ambiguous |
5 |
Repository is recovering or error, or state evidence is inconsistent |
6 |
Explicit Repository is not configured, or Base Generation is outside the valid range |
See also
sow build— create the next Generationsow publish— apply the supported publication protocolsow log— semantic Operations and their file actions- Repository Layout — public and private path boundaries
15 - sow publish
sow publish delivers one Repository’s current Built Generation to a named target from the
targets: map in sow.yml. The target binds its Repository and provider; the command does not accept
--repo or --dist.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
--abort |
Abandon a reconciled attempt that has not reached durable commit intent | false |
--rebind |
Confirm and record permitted target name/public endpoint/cache-TTL changes | false |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-T, --timeout DUR |
Maximum Repository-lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the sow.cli/v1 envelope |
false |
TARGET must name a configured filesystem or r2 publication target. --abort and
--rebind are mutually exclusive.
Publication protocol
Before delivery, SOW requires a completed Built Generation and verifies that the public tree is the exact frozen Generation manifest. It then plans and applies objects in this order:
- immutable payloads;
- checksum-addressed metadata;
- mutable protocol pointers;
- verification and durable checkpoint.
The exact object set, receipts, phase, and commit intent are recorded so an interrupted publication can be reconciled. Repeating a publication already at the current Generation is an idempotent no-op.
Rebind mutable target settings
The first successful publish durably binds a Repository, storage namespace, and target identity.
Later configuration drift is rejected rather than silently adopted. When the diagnostic explicitly
points to --rebind, review the edit and confirm it with:
If the map key itself was renamed, invoke the new target name. Rebind preserves active attempt and checkpoint identities and appends an immutable operator-confirmed binding revision.
| May change | Immutable; configure a new target |
|---|---|
| target name | Repository identity |
public_endpoint |
provider, storage endpoint, or region |
max_cache_ttl |
bucket or prefix |
Rebind takes the same Workspace and Repository locks as publication and rechecks the immutable
fields inside the database transaction. It may resume an active commit-intent attempt forward.
It refuses a TTL change while target maintenance is pending, and refuses a filesystem
public_endpoint change during conditional-delete maintenance. A first bind must use ordinary
publish, not --rebind.
Abort and recovery
--abort is valid only before durable commit intent. SOW reconciles objects already created, keeps
the evidence required for later safety decisions, and abandons the attempt without copying or
deleting more remote objects.
After commit intent, recovery is forward-only. Rerun sow publish TARGET; do not use --abort.
Public visibility checks
Provider storage success is not enough: publication verifies the canonical public_endpoint before
recording its checkpoint. HTTP(S) targets use ordinary GET as the final authority. A no-cache probe
may accelerate revalidation, but only a later ordinary GET can pass. Stale content and missing
objects are retried up to max_cache_ttl; 408, 425, 429, and 5xx failures get a short bounded retry
window. Header wait and body idle progress have separate deadlines, and an oversized body fails
closed.
Filesystem targets may use file:// or HTTP(S). Their conditional-deletion absence check requires
the exact file identity for file://, or canonical 404/410 visibility for HTTP(S). R2 requires an
HTTP(S) public endpoint; R2 target GC remains report-only and does not perform remote deletion.
Safety boundaries
- SOW publishes only configured targets; there is no arbitrary destination argument.
- Unbuilt Desired changes are never included. A dirty Repository can therefore publish its previous
complete Built Generation; run
buildfirst when the target must reflect current Desired state. - Layout transitions and contradictory recovery evidence block publication. Decidable unfinished Dist work is recovered before the source Generation is selected.
- Object order protects package-manager pointers from referencing absent content.
- Publication does not make an external web server, bucket policy, DNS route, or cache correct; those remain deployment concerns.
Exit behavior
| Code | Trigger |
|---|---|
0 |
Publication completed or target was already current |
1 |
Filesystem, provider, network, verification, or binding conflict (including required rebind) |
2 |
Usage, Workspace discovery, or invalid sow.yml error |
4 |
Repository write lock unavailable |
5 |
Local or publication recovery evidence is inconsistent, or source is not deliverable |
6 |
Target is missing/unsafe, or another safety precondition rejects publish/abort/rebind |
See also
sow statusandsow check— decide whether the Built Generation is the one you intend to deliversow gc— conservative target maintenancesow.ymltargets — provider configuration- Publication Model — phases, receipts, recovery, and cache grace
16 - sow retain
sow retain manages explicit local Generation roots. retain add can freeze only the current Built
Generation; after later builds make it historical, its required package payloads remain protected.
Synopsis
GENERATION must be a decimal integer greater than zero.
retain add
Requires GENERATION to equal the current Built Generation, verifies it, freezes its manifest under
private Workspace state, and adds an explicit GC root. Older Generations cannot be recreated after
the fact with retain add.
The retained record protects payloads; it does not switch the current view or publish anything. Adding an already retained Generation is idempotent only when the verified record agrees with current evidence.
retain ls
Lists explicit retained records. It is read-only and therefore accepts neither lock options nor
--dist.
An empty list is a successful result.
retain rm
Removes only the explicit retained root:
It does not delete package bytes. Removing a Generation that is not retained is an idempotent no-op.
A later local sow gc may reclaim payloads only if no other safety root reaches
them.
Options
| Flag | Commands | Meaning |
|---|---|---|
-C, --workdir DIR |
all | Workspace discovery start directory |
-r, --repo NAME |
all | Select a Repository |
-T, --timeout DUR |
add, rm |
Maximum write-lock wait |
-N, --no-wait |
add, rm |
Fail immediately when locked |
--json |
all | Emit the sow.cli/v1 envelope |
Exit behavior
| Code | Trigger |
|---|---|
0 |
Requested operation completed, including an empty list |
1 |
Filesystem or runtime I/O failure |
2 |
Invalid Generation syntax, discovery error, or implicit Repository selection is ambiguous |
4 |
Write lock unavailable for add or rm |
5 |
Generation manifest or Repository state is inconsistent |
6 |
Explicit Repository is not configured, retain add does not name the current Built Generation, or another safety rule rejects the request |
See also
sow gc— consumes retention rootssow changes— inspect a Built Generation delta- Repository Layout — private retained-record location
17 - sow gc
sow gc has two deliberately separate modes. With no positional target it collects unreachable
local pool payloads. With TARGET it maintains one configured publication target.
Synopsis
| Flag | Meaning | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository for local GC only | selection rules |
-T, --timeout DUR |
Maximum Repository-lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the sow.cli/v1 envelope |
false |
gc TARGET -r NAME is a usage error because the target already binds a Repository. --dist is not
accepted in either mode.
Local GC
Local GC deletes only pool payloads unreachable from every safety root:
- the current Built Generation;
- explicit
retainrecords; - recovery and non-terminal Operation state;
- publication attempts and evidence;
- active maintenance work.
The operation is journaled. When payloads are removed, the Repository advances to a new Generation. When nothing is eligible, it is an idempotent no-op.
Target GC
Target maintenance is provider-specific and uses publication checkpoints, absence evidence, and configured cache grace:
| Provider | Behavior |
|---|---|
filesystem |
Conditionally delete eligible objects only after grace and recorded storage/public absence checks |
r2 |
Persist an exact report-only retained-candidate set; never issue object deletion |
A no-op means no maintenance is due, not that the target was exhaustively revalidated.
Exit behavior
| Code | Trigger |
|---|---|
0 |
GC completed or nothing was eligible |
1 |
Filesystem, provider, network, or runtime failure |
2 |
Usage, Workspace discovery, invalid sow.yml, or implicit Repository ambiguity |
4 |
Repository write lock unavailable |
5 |
Recovery, state, receipt, or manifest evidence is inconsistent |
6 |
Explicit Repository/target is not configured or safe, or deletion is rejected by a safety precondition |
See also
sow retain— create and remove explicit local rootssow publish— create target checkpoints and receipts- Publication Model — provider guarantees and cache grace
18 - sow export
SOW provides one export subcommand: sow export rpm-leaf. It creates an external, standalone
RPM repository whose repodata uses local pool/... hrefs.
Synopsis
| Argument | Requirement |
|---|---|
DIST |
Canonical configured RPM Dist name |
ARCH |
x86_64 or aarch64 |
DIR |
Absent or empty destination outside Repository, private-state, and filesystem-target roots |
| Flag | Meaning | Default |
|---|---|---|
--hardlink |
Use hard links for trusted, same-filesystem, read-only output | copy files |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a Repository | selection rules |
--json |
Emit the sow.cli/v1 envelope |
false |
The command accepts no --dist, jobs, timeout, or lock option.
Output
The destination contains:
- rewritten RPM repodata with local payload hrefs;
- the required package subtree;
- an export manifest;
.sow-export.jsonprovenance.
The source must be a completed Built Generation. The export is a separate artifact: it is not Desired Membership, a Built Generation, publication input, or a GC root.
Copy versus hard link
Copying is the safe default. --hardlink is an explicit optimization for an output on the same
filesystem that consumers cannot mutate. A hard-linked payload shares an inode with SOW’s pool; do
not use this mode for writable or untrusted destinations.
SOW rejects output that overlaps a configured filesystem publication root. This prevents an export from being mistaken for, or modifying, a managed publication target.
Exit behavior
| Code | Trigger |
|---|---|
0 |
Standalone RPM leaf exported |
1 |
Filesystem, copy, hard-link, or metadata-write failure |
2 |
Invalid syntax, malformed Dist/architecture token, discovery, or implicit Repository ambiguity |
5 |
Source Generation or Repository state is inconsistent |
6 |
Explicit Repository is not configured, Dist is not RPM, view/signer is unavailable, or destination is unsafe/non-empty/overlapping |
See also
- Platforms & Integrations — validated and unsupported workflows
- Repository Layout — source tree and export boundaries
sow publish— managed delivery to configured targets
19 - sow log
Every write command inside a Repository commits an application-level Operation to that Repository’s
SQLite database before it produces any external file side effect. That record is what makes crash
recovery possible — and once the Operation reaches a terminal state, the same record is your audit
trail. sow log reads it.
Synopsis
Operation lifecycle
Understanding the state field is most of understanding the log:
| State | Meaning |
|---|---|
planned |
Command, arguments, targets and intended actions are durably recorded |
staged |
New packages/metadata written to temporary locations and verified |
applied |
Desired state and required private pending payloads committed |
built |
The complete static Generation has been switched in |
done |
Terminal — a normal successful command |
done_dirty |
Terminal — --skip was given, so the public tree deliberately stayed behind |
failed |
Terminal — the operation failed before applied, nothing was committed |
rolled_back |
Terminal — a post-applied failure the process could safely undo |
recovering |
Non-terminal; the next write command must complete or roll it back |
Workspace lifecycle commands (init, repo new, repo rm) use the Workspace file journal instead
and do not appear in a Repository’s SQLite log. dist new/dist rm do appear — the Repository
database already exists at that point.
sow log
With no argument, prints the 50 most recent Operations, newest first.
Output excerpt, one Operation object from the operations array:
payload_json records the intent — including config_sha256, the digest of the configuration in
force, and manifest_sha256 for the resulting Generation. result_json records the outcome. A
failed Operation additionally carries error_class and error_message:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Show only Operations touching this Dist | all |
--json |
Emit the versioned JSON envelope | false |
One Operation in detail
Pass an Operation ID to get its full state transitions, timing, packages, memberships and file actions.
Output excerpt:
The files array uses the same phase vocabulary as
sow changes: payload, metadata, pointer, delete.
Build Operations also contain progress events. They keep the current state and put a
versioned object in detail_json:
The phases are rendering, promoting_payload, publishing_dists,
normalizing_public_tree, and finalizing. A progress row is durable audit data but does
not advance the recovery state machine or force its own SQLite checkpoint.
Filtering by Dist
-d restricts the listing to Operations that touched that Dist — useful when one Repository serves
several distributions:
sow log export
Writes terminal Operations as JSONL — one complete Operation detail record per line — for archival or ingestion into a log pipeline.
Omit FILE, or pass -, to write to stdout:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Export only Operations touching this Dist | all |
export has no --json; JSONL is its output format.
It refuses to clobber
An existing target is a rejection, never an overwrite — an audit export must not silently destroy a previous one:
export also refuses a target whose parent is not a real directory — a symlink, or a directory that
does not exist:
On macOS /tmp is a symlink to /private/tmp, so that refusal fires there. Write to an explicit
real path instead.
sow log prune
Deletes eligible terminal audit records older than BEFORE and safely compacts the database.
The absolute timestamp is echoed back so the local-timezone interpretation is never ambiguous.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
prune operates at Repository level and does not accept -d — pruning half an Operation would
produce a meaningless record.
BEFORE syntax
BEFORE is an ISO-8601 date YYYY-MM-DD, interpreted as local midnight, or an RFC 3339 timestamp
with a timezone.
What prune never deletes
prune is conservative by construction. It never removes:
- a non-terminal Operation;
- a record still required for recovery;
- current Package or Membership state;
- a Built Generation or its Changeset.
The pruned counter tells you exactly how many records were eligible, which is normally fewer than
the number of Operations older than the cutoff. Log and Changeset live in the same SQLite database,
but they follow different retention rules.
Examples
Investigate the most recent write:
List everything that failed:
Archive and shrink, monthly:
Which Operation last touched a Dist:
Exit codes
| Command | Code | Trigger |
|---|---|---|
log |
0 |
Records printed, including an empty ledger |
log |
2 |
Usage error (including a non-numeric Operation ID), Workspace not found, or ambiguous selection |
log |
5 |
State database unreadable |
log |
6 |
The given Operation ID does not exist |
log export |
0 |
Export written |
log export |
1 |
I/O failure writing the target, or the parent is not a real directory |
log export |
2 |
Usage error or ambiguous selection |
log export |
6 |
Target already exists |
log prune |
0 |
Prune completed, including pruning nothing |
log prune |
2 |
Malformed BEFORE, -d given, or ambiguous selection |
log prune |
4 |
Repository lock held and --no-wait given or --timeout expired |
log prune |
5 |
Integrity or recovery error |
See also
- Observability & Audit — how
logfits withstatus,checkandchanges - Transactions & Recovery — the journal that the log records
- sow changes — the physical counterpart to a semantic Operation
- JSON Output — the full log result schema