Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Commands

Complete SOW CLI syntax, options, behavior, output, and exit codes.

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

sow [OPTIONS] COMMAND [ARGS]

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:

  1. Start at -C/--workdir DIR, when supplied; otherwise start at the current directory.
  2. Search upward and stop at the first sow.yml.
  3. If that search finds nothing, repeat from SOW_DIR when set. An explicit -C suppresses the current-directory candidate, but not the SOW_DIR fallback.
  4. 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:

  1. explicit -r/--repo NAME;
  2. the Repository containing the discovery start directory;
  3. the only Repository in the Workspace;
  4. otherwise fail with exit 2 and 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:

  1. repeated -d/--dist NAME values;
  2. the Dist containing the discovery start directory;
  3. the only Dist in the selected Repository;
  4. otherwise fail with exit 2 and list the candidates.

Other commands deliberately differ:

  • build, check, and status default to all Dists when -d is absent;
  • show searches the whole selected Repository unless -d narrows it;
  • where searches all matching Dists across the Workspace unless -r/-d narrow it;
  • changes is 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:

{
  "schema": "sow.cli/v1",
  "command": "add",
  "ok": true,
  "repository": "demo",
  "operation": "1430722512865805553",
  "result": {},
  "errors": []
}

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

Generate a flat RPM/DEB repository in an ordinary directory — the Plain mode entry point.

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

sow create [DIR] [-j N] [--pigsty] [-S KEY [--overwrite]] [-T DUR | -N] [--json]

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 .rpm or .deb are 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. --pigsty accepts an empty authoritative set so an interrupted all-package cleanup can converge and write its marker.
sow create /srv/empty
plain: scan /srv/empty: no supported top-level regular RPM or DEB packages

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:

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=false recovered=false

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=true recovered=false

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:

sow create /srv/pigsty
plain: marker gate /srv/pigsty/repo_complete: repo_complete exists; use --pigsty or remove it explicitly before rebuilding

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:

  1. Delete DEB packages whose parsed architecture is i386. RPMs are not removed merely for carrying an i386/i486/i586/i686 architecture.
  2. Delete RPM/DEB whose binary package name is exactly patroni and whose upstream version is exactly 3.0.4. RPM compares VERSION, ignoring epoch and release; DEB strips epoch and Debian revision first. 3.0.4+foo is not a match.
  3. 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>.
sow create /srv/pigsty --pigsty
created /srv/pigsty: rpm=2 deb=0 signed=0 removed=2 marker=true noop=false recovered=false
cat /srv/pigsty/repo_complete
b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead  centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm
d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab  epel-release-7-5.noarch.rpm

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.

Marker semantics

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.
  • --overwrite requires --sign-with and switches to rpm --resign over 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 --pigsty cleanup, and rpm must be on PATH.
sow create /srv/flat -S 0123456789ABCDEF --overwrite
plain: sign rpm epel-release-7-5.noarch.rpm: rpm executable is required for --sign-with
sow create /srv/deb-only -S 0123456789ABCDEF
plain: sign rpm: --sign-with requires at least one retained top-level RPM package
sow create /srv/flat --overwrite
usage error: --overwrite requires --sign-with
sow create /srv/flat -S ZZZZ
usage error: --sign-with must be a 16, 40, or 64 hexadecimal GPG key ID/fingerprint

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:

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=false recovered=false
ls /srv/flat
centos-release-6-0.el6.centos.5.x86_64.rpm
centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm
epel-release-7-5.noarch.rpm
libpq5_18.3-1_amd64.deb
Packages
Packages.gz
repodata

Machine-readable result:

sow create /srv/flat --json
{"schema":"sow.cli/v1","command":"create","ok":true,"repository":null,"operation":null,"result":{"dir":"/srv/flat","rpm":3,"deb":1,"kept":["centos-release-6-0.el6.centos.5.x86_64.rpm","centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm","epel-release-7-5.noarch.rpm","libpq5_18.3-1_amd64.deb"],"removed":[],"marker":false,"noop":true,"recovered":false},"errors":[]}

Replace a Pigsty plain build with eight workers:

sow create /www/pigsty -j 8 --pigsty

Failure envelope:

sow create /srv/empty --json
{"schema":"sow.cli/v1","command":"create","ok":false,"repository":null,"operation":null,"result":{"dir":"","rpm":0,"deb":0,"kept":null,"removed":null,"marker":false,"noop":false,"recovered":false},"errors":[{"code":6,"class":"rejected","message":"operation rejected: plain: scan /srv/empty: no supported top-level regular RPM or DEB packages"}]}

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

2 - sow init

Create a Workspace, and converge whatever Repositories and Dists sow.yml already declares.

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

sow init [DIR] [--json]

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 init .
initialized /srv/repo: config_created=true repositories_initialized=0 dists_initialized=0
cat sow.yml
schema: sow/v3
architectures:
  - x86_64
  - aarch64
ls -a /srv/repo
.  ..  .sow  sow.yml

.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:

  1. It writes schema: sow/v3 and the default architectures: [x86_64, aarch64] when creating a new config.

  2. It never creates a Repository on its own. Use sow repo new, or declare one in sow.yml first.

  3. It never overwrites an existing sow.yml. A repeat run reports the current state and lists what it found:

    sow init .
    initialized /srv/repo: config_created=false repositories_initialized=0 dists_initialized=0
    
  4. 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.

schema: sow/v3
architectures: [x86_64, aarch64]

repos:
  pgsql:
    dists:
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource]
      trixie:
        format: deb
  infra:
    protected: true
    dists:
      el9:
        format: rpm
sow init .
initialized /srv/repo: config_created=false repositories_initialized=2 dists_initialized=3
sow repo ls
NAME	PROTECTED	DISTS	GENERATION	STATUS	PACKAGES	MEMBERSHIPS
infra	true	1	1	clean	0	0
pgsql	false	2	2	clean	0	0

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:

sow init . --json
{"schema":"sow.cli/v1","command":"init","ok":true,"repository":null,"operation":null,"result":{"workspace":"/srv/repo","config_created":false,"repositories_initialized":0,"dists_initialized":0,"existing":["sow.yml"]},"errors":[]}

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:

mkdir -p /srv/repo && cd /srv/repo
sow init
sow repo new infra
sow repo new pgsql
sow dist new el9 --format rpm -r pgsql
sow dist new trixie --format deb -r pgsql

Initialize a directory other than the current one:

sow init /srv/repo

Provision from a config file under version control:

install -m 0644 sow.yml /srv/repo/sow.yml
sow init /srv/repo
sow config check -C /srv/repo

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

3 - sow config

Validate sow.yml without touching anything, and print the effective configuration for any scope.

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 config check [-C DIR] [--json]
sow config show [--all] [-C DIR] [-r NAME] [-d NAME]... [--json]

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.

sow config check
configuration valid: /srv/repo repositories=1 dists=2
sow config check --json
{"schema":"sow.cli/v1","command":"config check","ok":true,"repository":null,"operation":null,"result":{"workspace":"/srv/repo","repositories":1,"dists":2},"errors":[]}

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:

sow config check
configuration error: load config "/srv/repo/sow.yml": parse sow.yml: yaml: unmarshal errors:
  line 8: field bogus_field not found in type config.DistConfig

The schema version is pinned:

sow config check
configuration error: load config "/srv/repo/sow.yml": config schema must be "sow/v3", got "invalid"

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.

sow config show
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    protected: false
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []
      trixie:
        format: deb
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []

Compare that with the file on disk, which carries only what you wrote:

cat sow.yml
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
      trixie:
        format: deb

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”:

sow config show -r pigsty -d el9
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    protected: false
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []

--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:

sow config check -C /srv/repo || exit 1
sow build -r pgsql

Diff effective policy across two Dists:

sow config show -r pgsql -d el9 > /tmp/el9.yml
sow config show -r pgsql -d el9-beta > /tmp/beta.yml
diff -u /tmp/el9.yml /tmp/beta.yml

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

4 - sow repo

List, create, inspect and remove Repositories — the lock, transaction and Generation boundary.

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

sow repo ls [-C DIR] [--json]
sow repo new NAME [-C DIR] [-T DUR | -N] [--json]
sow repo show [NAME] [-C DIR] [-r NAME] [--json]
sow repo migrate [NAME] [--abort] [-j N] [-C DIR] [-r NAME] [-T DUR | -N] [--json]
sow repo rm NAME [-f|--force] [-C DIR] [-T DUR | -N] [--json]

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.

sow repo new .sow
operation rejected: managed: operation rejected: name ".sow" must match [a-z0-9][a-z0-9._-]*

You cannot choose the path. A Repository always lives at <workspace>/<NAME>/.

sow repo ls

Read-only listing of every Repository in the Workspace.

sow repo ls
NAME	PROTECTED	DISTS	GENERATION	STATUS	PACKAGES	MEMBERSHIPS
infra	true	1	1	clean	0	0
pgsql	false	2	2	clean	0	0
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.

sow repo new pigsty
created pigsty: path=/srv/repo/pigsty protected=false dists=0 generation=0 status=clean packages=0 memberships=0
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.

sow repo show pigsty
repository pigsty:
  path: /srv/repo/pigsty
  protected: false
  dists: 2
  generation: 6
  desired_revision: 6
  status: clean
  packages: 5
  memberships: 8
  config: {"protected":false,"signing":{"rpm":{"packages":{"mode":"never"}}},"dists":{"el9":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":1,"exclude":[{"kind":["debuginfo","debugsource"]}]},"trixie":{"format":"deb","architectures":["x86_64","aarch64"],"limit":0,"exclude":null}}}
  dirty_reasons: []
  recent_operation: id=4142220455201181493 kind=add state=done error_class= created_at=2026-08-04T04:09:24.995538Z updated_at=2026-08-04T04:09:25.332772Z
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 show demo -r empty
operation rejected: repo show NAME "demo" and --repo "empty" select different repositories

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.

cp -a /srv/sow /srv/sow.backup-before-0.4.0
sow repo migrate pigsty -C /srv/sow
sow repo migrate pgsql -C /srv/sow

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:

sow repo rm infra
removed repository infra
sow repo rm pgsql
operation rejected: managed: operation rejected: repository "pgsql" is not empty; use --force
sow repo rm pgsql -f
removed repository pgsql
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:

sow repo rm alpha -f
operation rejected: managed: operation rejected: repository "alpha" is protected

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:

sow dist rm el9 -r alpha -f
removed dist el9 from alpha

Examples

Create the Repositories for a two-tier layout:

sow repo new infra
sow repo new pgsql

Fail fast in a cron job rather than queue behind another writer:

sow repo new nightly -N || echo "another writer holds the workspace lock"

Audit every Repository in one line each:

sow repo ls --json | jq -r '.result.repositories[] | "\(.name)\t\(.status)\tgen=\(.generation)"'

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

5 - sow dist

List, create, inspect and remove Dists — the named single-format member set clients actually consume.

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

sow dist ls [-C DIR] [-r NAME] [--json]
sow dist new NAME --format rpm|deb [-C DIR] [-r NAME] [-T DUR | -N] [--json]
sow dist show NAME [-C DIR] [-r NAME] [--json]
sow dist rm NAME [-f|--force] [-C DIR] [-r NAME] [-T DUR | -N] [--json]

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.

sow dist ls -r pigsty
NAME	FORMAT	ARCHITECTURES	DESIRED	BUILT	GENERATION	DIRTY	DIRTY_REASONS
el9	rpm	x86_64,aarch64	0	0	1	false	[]
trixie	deb	x86_64,aarch64	0	0	2	false	[]

DESIRED and BUILT are membership counts. When they diverge, DIRTY_REASONS says why:

sow dist ls -r demo
NAME	FORMAT	ARCHITECTURES	DESIRED	BUILT	GENERATION	DIRTY	DIRTY_REASONS
el9	rpm	x86_64,aarch64	2	1	4	true	["Desired and Built membership sets differ"]
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:

"architectures":[{"family":"x86_64","ecosystem_arch":"amd64"},{"family":"aarch64","ecosystem_arch":"arm64"}]

sow dist new

Creates an ordinary, still-editable Dist. The only business argument is --format.

sow dist new el9 --format rpm -r pigsty
created el9: format=rpm architectures=x86_64,aarch64 members=0/0 generation=1 dirty=false
sow dist new trixie --format deb -r pigsty
created trixie: format=deb architectures=x86_64,aarch64 members=0/0 generation=2 dirty=false
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:

sow dist new x -r alpha
usage error: dist new requires --format rpm|deb
sow dist new x --format zip -r alpha
usage error: --format must be rpm or deb

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:

sow dist new el9 --format deb -r alpha
operation rejected: managed: operation rejected: dist "el9" already exists with format rpm

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.

sow dist show trixielim -r pgsql
dist trixielim:
  format: deb
  architectures: x86_64,aarch64
  desired_members: 3
  built_members: 3
  generation: 6
  status: clean
  dirty: false
  dirty_reasons: []
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 show el9 -r pgsql --json
{"schema":"sow.cli/v1","command":"dist show","ok":true,"repository":"pgsql","operation":null,"result":{"name":"el9","format":"rpm","architectures":[{"family":"x86_64","ecosystem_arch":"x86_64"},{"family":"aarch64","ecosystem_arch":"aarch64"}],"desired_members":0,"built_members":0,"generation":"00000000000000000001","dirty":false,"status":"clean","effective_config_sha256":"a0b3ae2f943bc4fce951aaadda0fc8fb146ccf7944b0193a0dcc2b86ddc7ce7e","config":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":1,"exclude":[{"kind":["debuginfo","debugsource"]}]}},"errors":[]}
sow dist show nope -r demo
operation rejected: managed: operation rejected: dist "nope" does not exist

sow dist rm

Removes a Dist’s Membership and derived indexes.

sow dist rm el9 -r pgsql
operation rejected: managed: operation rejected: dist "el9" is not empty; use --force
sow dist rm el9 -r pgsql -f
removed dist el9 from pgsql
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:

sow dist rm el9 -r pgsql -f
removed dist el9 from pgsql

find pgsql -type f
pgsql/pool/e/epel-release/epel-release-7-5.noarch.rpm

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:

sow dist new el9 --format rpm -r pgsql
sow dist new trixie --format deb -r pgsql

Add a beta channel with its own retention policy — create it, then set the policy in sow.yml and converge:

sow dist new el9-beta --format rpm -r pgsql
$EDITOR sow.yml          # el9-beta: { limit: 0 }
sow config check
sow build -r pgsql -d el9-beta

Which Dists are behind their Desired state:

sow dist ls -r pgsql --json | jq -r '.result.dists[] | select(.dirty) | .name'

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

6 - sow add

Add packages to Desired Membership, apply policy, and rebuild the affected indexes.

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

sow add PATH... [-R|--recursive] [--skip] [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]

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.

sow add /srv/pkg/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm /srv/pkg/epel-release-7-5.noarch.rpm -r pigsty -d el9
add repository=pigsty operation=8677129233475584643 accepted=2 failed=0 memberships=+2/-0 revision=3 generation=3 dirty=false
item input="/srv/pkg/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm" status=accepted format=rpm coordinate="centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead dists=el9:accepted
item input="/srv/pkg/epel-release-7-5.noarch.rpm" status=accepted format=rpm coordinate="epel-release-0:7-5.noarch" sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab dists=el9:accepted

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:

sow add /srv/pkg/epel-release-7-5.noarch.rpm -r pigsty -d el9
add repository=pigsty operation=656950149626836753 accepted=1 failed=0 memberships=+0/-0 revision=4 generation=4 dirty=false
item input="/srv/pkg/epel-release-7-5.noarch.rpm" status=reused format=rpm coordinate="epel-release-0:7-5.noarch" sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab dists=el9:accepted

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:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm -r pigsty -d el9
item input="/srv/pkg/centos-release-3.1-1.i386.rpm" status=failed error="managed: operation rejected: unknown rpm package architecture \"i386\"; supported rpm package architectures are [x86_64, aarch64, noarch] (canonical families [x86_64, aarch64, neutral]); use a supported package or update only supported architecture families in sow.yml"

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.

sow add /srv/pkg/debs -r pgsql -d trixielim
add repository=pgsql operation=4142220455201181493 accepted=3 failed=0 memberships=+3/-0 revision=6 generation=6 dirty=false
item input="/srv/pkg/debs/libpq5-dbgsym_18.3-1_amd64.deb" status=excluded format=deb coordinate="libpq5-dbgsym=18.3-1:amd64" sha256:cf491b9d9b218fa49ad2b41b4740d62cd972e1b515bf33677c2c3ead75acc60a dists=trixielim:excluded
item input="/srv/pkg/debs/libpq5_18.2-1_amd64.deb" status=excluded format=deb coordinate="libpq5=18.2-1:amd64" sha256:fa84dc641b7c686be2f9b512311ad0b74eac03e2afc9eff7e9af75b82b68ff41 dists=trixielim:limited
item input="/srv/pkg/debs/libpq5_18.3-1_amd64.deb" status=reused format=deb coordinate="libpq5=18.3-1:amd64" sha256:491992c502113627d44d0d66a2b189cdaa8accff293ebaf84fe10ccbc9da574c dists=trixielim:accepted
item input="/srv/pkg/debs/libpq5_18.3-1_arm64.deb" status=reused format=deb coordinate="libpq5=18.3-1:arm64" sha256:3a2f7ef7cddfa3dc06280ef59eda1dab9724d57499931ee80758b11531c1f40c dists=trixielim:accepted
item input="/srv/pkg/debs/pg-sample_1.17-1_all.deb" status=reused format=deb coordinate="pg-sample=1.17-1:all" sha256:f23581c5164a143e5e902232589adf1d30b73ba3857a692a11da607f246aacc3 dists=trixielim:accepted

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.

Policy removals do not come back

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:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm /srv/pkg/centos-release-6-0.el6.centos.5.x86_64.rpm -r pigsty -d el9
add repository=pigsty operation=4623871845694427260 accepted=1 failed=1 memberships=+1/-0 revision=5 generation=5 dirty=false
item input="/srv/pkg/centos-release-3.1-1.i386.rpm" status=failed error="managed: operation rejected: unknown rpm package architecture \"i386\"; ..."
item input="/srv/pkg/centos-release-6-0.el6.centos.5.x86_64.rpm" status=accepted format=rpm coordinate="centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 dists=el9:accepted
managed: batch partially succeeded

If nothing is accepted, the whole operation is rejected with exit 6 and the Repository is unchanged:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm -r pigsty -d el9
operation rejected: managed: operation rejected: no input package was accepted

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.

sow add /srv/pkg/tree -R --skip -r pgsql -d trixie
add repository=pgsql operation=8405631664133415270 accepted=6 failed=0 memberships=+4/-0 revision=4 generation=3 dirty=true
sow status -r pgsql
repository=pgsql status=dirty ready_to_copy=false revision=4 generation=3 dirty_dists=trixie pending=4/2326 locked=false

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:

sow add /srv/build/ -R -r pgsql -d el9 --skip
sow status -r pgsql
sow build -r pgsql -j 12
sow check -r pgsql

Processing order

For the record, one add executes in this order:

  1. Take the Repository write lock and recover any unfinished Operation.
  2. Commit a planned Operation in SQLite.
  3. Parse inputs read-only; compute logical coordinates and input SHA-256 (plus, for RPM, the signature-neutral payload digest).
  4. Check the architecture permit list and look up existing coordinates.
  5. 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.
  6. Merge target Memberships, then apply exclude and limit over the complete Dist set.
  7. Commit the Desired state; new bytes go to the private pending content store.
  8. Unless --skip, publish still-needed pending objects into pool/ 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

7 - sow rm

Remove Desired Membership from selected Dists, with a no-write preview mode.

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

sow rm PACKAGE... [-c|--check] [--skip] [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]

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:

sow rm epel-release -c --skip
usage error: --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:

sow rm nosuch -r pigsty -d el9
operation rejected: managed: operation rejected: package reference not found: package reference "nosuch" matches no Desired Membership

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.

sow rm centos-release -r demo -d el9 -c
preview repository=demo operation= dists=el9 memberships=2 revision=2 generation=00000000000000000003 dirty=false changes=2
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead
change op=update phase=pointer path="dists/el9/aarch64/repodata/repomd.xml" size=1509 sha256:1cfe38698967d11384f1a985618d75f5e690d1284accf951262fc663fa9afc81
change op=update phase=pointer path="dists/el9/x86_64/repodata/repomd.xml" size=1509 sha256:1cfe38698967d11384f1a985618d75f5e690d1284accf951262fc663fa9afc81

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:

sow rm centos-release -r pigsty -d el9 -c -T 5s
usage error: rm --check does not accept --timeout or --no-wait

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.

sow rm 'rpm:centos-release-0:6-0.el6.centos.5.x86_64' -r demo -d el9
removed repository=demo operation=2283442100870457321 dists=el9 memberships=1 revision=2 generation=00000000000000000003 dirty=false changes=8
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16

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.

sow rm 'rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64' --skip -r demo -d el9
removed repository=demo operation=314678479940914827 dists=el9 memberships=1 revision=4 generation=00000000000000000004 dirty=true changes=0
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead
sow status -r pigsty
repository=pigsty status=dirty ready_to_copy=false revision=6 generation=5 dirty_dists=el9 pending=0/0 locked=false

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:

sow rm patroni -r pgsql -d el9 -c
sow rm patroni -r pgsql -d el9

Remove one exact object from two Dists at once:

sow rm sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab -r pgsql -d el9 -d el9-beta

Batch several removals, then rebuild once:

sow rm old-tool legacy-agent -r pgsql -d el9 --skip
sow build -r pgsql -d el9
sow check -r pgsql

Feed the preview plan to another tool:

sow rm patroni -r pgsql -d el9 -c --json | jq -r '.result.changes[] | "\(.phase)\t\(.op)\t\(.path)"'

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

8 - sow ls

List Desired and Built package membership for the selected Dists.

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

sow ls [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
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

sow ls -r pigsty -d el9
repository=pigsty dists=el9 dirty=false
SHA256	COORDINATE	DISTS	BUILT_DISTS	POOL_PATH
sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab	rpm:epel-release-0:7-5.noarch	el9	el9	pool/e/epel-release/epel-release-7-5.noarch.rpm
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>/.

sow ls -r pigsty
workspace discovery error: managed: workspace discovery or configuration error: repository "pigsty" has multiple Dists (el9, trixie); select one or more with --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:

sow ls -r pgsql -d el9 --json |
  jq -r '.result.packages[] | select(.built_dists | length == 0) | .sha256'

List pool paths in deterministic order:

sow ls -r pgsql -d el9 --json | jq -r '.result.packages[].pool_path' | sort

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

9 - sow show

Inspect one Package Object, including identity, normalized facts, storage, signature, and membership.

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

sow show PACKAGE [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
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:

sow show libpq5 -r pgsql -d trixie
operation rejected: managed: operation rejected: package reference "libpq5" is ambiguous: deb:libpq5=18.2-1:amd64 sha256:fa84dc64..., deb:libpq5=18.3-1:amd64 sha256:491992c5..., deb:libpq5=18.3-1:arm64 sha256:3a2f7ef7...

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:

sow show centos-release-6-0.el6.centos.5.x86_64.rpm -r demo -d el9
package repository=demo coordinate="centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 format=rpm architecture=x86_64 size=19776 storage=pool
pool=pool/c/centos-release/centos-release-6-0.el6.centos.5.x86_64.rpm
dists=el9 built_dists=el9

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 Membership
  • sow where — search across Repositories
  • sow rm — remove matching Desired Membership
  • JSON Output — complete result schema

10 - sow where

Locate one Package Object across Repositories and Dists in a Workspace.

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

sow where PACKAGE [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
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:

sow where 'rpm:centos-release-0:6-0.el6.centos.5.x86_64'
reference="rpm:centos-release-0:6-0.el6.centos.5.x86_64" locations=1
repository=demo coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 dists=el9 built_dists=el9

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:

sow where nosuchpkg
operation rejected: managed: operation rejected: package reference "nosuchpkg" was not found in the selected Workspace scope

Example

List every location still serving an exact build:

sow where 'rpm:patroni-0:3.0.4-1.noarch' --json |
  jq -r '.result.locations[] | "\(.repository)/\(.dists | join(","))"'

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

11 - sow status

Read Repository convergence, readiness, pending payload, recent Operation, and lock state without deep verification.

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

sow status [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
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

sow status -r pgsql
repository=pgsql status=dirty ready_to_copy=false revision=4 generation=3 dirty_dists=trixie pending=4/2326 locked=false

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:

{
  "repository": "demo",
  "status": "dirty",
  "ready_to_copy": false,
  "desired_revision": 5,
  "built_generation": "00000000000000000004",
  "dirty_dists": ["el9"],
  "dirty_reasons": ["dist el9 Desired and Built membership sets differ"],
  "pending": {"count": 1, "bytes": 19776},
  "repository_locked": false
}

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

12 - sow build

Converge Desired Membership and renderer configuration into a complete Built Generation.

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

sow build [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]
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:

sow build -r demo -d el9
built repository=demo operation=2769214987359113555 dists=el9 revision=4 generation=00000000000000000005 dirty=false

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:

sow build -r demo -d el9
build repository=demo dists=el9 already current (noop) revision=4 generation=00000000000000000005 dirty=false

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; writes repomd.xml.asc when configured.
  • DEB: always writes Release; writes InRelease and Release.gpg when 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

13 - sow check

Run the full read-only integrity and delivery-readiness verification pipeline.

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

sow check [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
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.

sow check
repository=pigsty status=clean ready_to_copy=true revision=5 generation=5
config	ok=true	checked=5
state	ok=true	checked=1
public-modes	ok=true	checked=67
retained	ok=true	checked=0
package-bytes	ok=true	checked=8
desired-membership	ok=true	checked=8
index	ok=true	checked=2
signature	ok=true	checked=9
generation-manifest	ok=true	checked=1

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:

sow check
repository=pigsty status=dirty ready_to_copy=false revision=6 generation=5
...
integrity or recovery error: managed: repository is not ready to copy: repository status is dirty

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

14 - sow changes

Diff Built Generations as a deterministic Repository-relative file delivery plan.

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

sow changes [BASE_GENERATION] [-C|--workdir DIR] [-r|--repo NAME] [--json]
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

sow changes
base=4 generation=5 dirty=false
add	payload	pool/c/centos-release/centos-release-6-0.el6.centos.5.x86_64.rpm	19776	ffd9e7bd...
add	metadata	dists/el9/x86_64/repodata/5bc463cb...-primary.xml.gz	1460	5bc463cb...
update	pointer	dists/el9/x86_64/repodata/repomd.xml	1514	05d3d5bf...
delete	delete	dists/el9/x86_64/repodata/0df96f0b...-primary.xml.gz	0	

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.

sow changes 99
operation rejected: managed: operation rejected: base generation 99 is outside 0..2

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:

sow changes 0 -r pgsql --json > pgsql-current.json

Filter one Dist by path after producing the Repository-level plan:

sow changes -r pgsql --json |
  jq '.result.changes[] | select(.path | startswith("dists/el9/"))'

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

15 - sow publish

Publish the current verified Generation to a configured filesystem or R2 target.

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

sow publish TARGET [--abort | --rebind] [-C|--workdir DIR] [-T|--timeout DUR | -N|--no-wait] [--json]
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:

  1. immutable payloads;
  2. checksum-addressed metadata;
  3. mutable protocol pointers;
  4. 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.

sow publish local
published demo generation=00000000000000000005 to local (filesystem): phase=grace objects=14
sow publish local
publication demo generation=00000000000000000005 to local is already current (noop)

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:

sow publish prod --rebind

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 build first 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

16 - sow retain

Add, list, and remove explicit retained-Generation roots for local garbage collection.

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

sow retain add GENERATION [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]
sow retain ls             [-C|--workdir DIR] [-r|--repo NAME] [--json]
sow retain rm GENERATION  [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]

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.

sow retain add 12 -r pgsql
retained generation 00000000000000000012: /srv/sow/.sow/pgsql/retained/00000000000000000012

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.

sow retain ls -r pgsql
GENERATION	RECORD_IDENTITY	PATH
00000000000000000012	678beeae...	/srv/sow/.sow/pgsql/retained/00000000000000000012

An empty list is a successful result.

retain rm

Removes only the explicit retained root:

sow retain rm 12 -r pgsql
removed retained generation 00000000000000000012

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

17 - sow gc

Collect unreachable local payloads or perform conservative maintenance for one publication target.

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

sow gc          [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]
sow gc TARGET   [-C|--workdir DIR]                  [-T|--timeout DUR | -N|--no-wait] [--json]
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 retain records;
  • 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.

sow gc -r pgsql
local gc pgsql: generation=00000000000000000013 objects=4 bytes=1834200

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
sow gc prod
target gc pgsql/prod (filesystem): phase=done candidates=14 deleted=8 retained=6 pending=0

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

18 - sow export

Export one built RPM Dist architecture as a standalone compatibility repository.

SOW provides one export subcommand: sow export rpm-leaf. It creates an external, standalone RPM repository whose repodata uses local pool/... hrefs.

Synopsis

sow export rpm-leaf DIST ARCH DIR [--hardlink] [-C|--workdir DIR] [-r|--repo NAME] [--json]
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

sow export rpm-leaf el9 x86_64 /srv/export/el9-x86_64
exported RPM leaf el9/x86_64 generation=00000000000000000012 method=copy packages=84 to /srv/export/el9-x86_64

The destination contains:

  • rewritten RPM repodata with local payload hrefs;
  • the required package subtree;
  • an export manifest;
  • .sow-export.json provenance.

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.

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

19 - sow log

Read the Operation audit ledger, export it as JSONL, and prune eligible terminal records.

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

sow log [OPERATION] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME] [--json]
sow log export [FILE] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]
sow log prune BEFORE [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]

Operation lifecycle

Understanding the state field is most of understanding the log:

planned → staged → applied → built → done
                        └────────→ done_dirty
   any nonterminal → recovering → built / rolled_back
   pre-apply error  → failed
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.

sow log -r pigsty

Output excerpt, one Operation object from the operations array:

{
  "id": "4262183287563704350",
  "kind": "build",
  "state": "done",
  "payload_json": "{\"version\":2,\"repository\":\"pigsty\",\"kind\":\"build\",\"config_sha256\":\"37eb6dcf...\",\"skip\":false,\"dists\":[\"el9\"],\"build_dists\":[\"el9\"],\"manifest_sha256\":\"678beeae...\"}",
  "result_json": "{\"dists\":1,\"dropped_pending\":[]}",
  "created_at": "2026-08-04T04:07:40.334787Z",
  "updated_at": "2026-08-04T04:07:40.907125Z"
}

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:

{
  "id": "5995346754219751025",
  "kind": "add",
  "state": "failed",
  "result_json": "{\"accepted\":0,\"failed\":1}",
  "error_class": "rejected",
  "error_message": "no input package was accepted"
}
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.

sow log 4262183287563704350 -r pigsty

Output excerpt:

{
  "duration_ms": 572,
  "events": [
    {"sequence": 0, "state": "planned",  "occurred_at": "2026-08-04T04:07:40.334787Z"},
    {"sequence": 1, "state": "staged",   "occurred_at": "2026-08-04T04:07:40.380963Z"},
    {"sequence": 2, "state": "applied",  "occurred_at": "2026-08-04T04:07:40.386186Z"},
    {"sequence": 3, "state": "built",    "occurred_at": "2026-08-04T04:07:40.904730Z"},
    {"sequence": 4, "state": "done",     "occurred_at": "2026-08-04T04:07:40.907125Z"}
  ],
  "packages": [],
  "memberships": [],
  "files": [
    {"sequence": 0, "action": "update", "phase": "pointer", "path": "dists/el9/aarch64/repodata/repomd.xml", "size": 1511, "sha256": "ef071821e06c9e86ab4f6d2a56906d82bb66df251e79d1086cfd44dc8395513e"},
    {"sequence": 1, "action": "update", "phase": "pointer", "path": "dists/el9/x86_64/repodata/repomd.xml",  "size": 1514, "sha256": "a31e90ec39169f0373b108458908333c96c5f600f3c63a50c44257856f0d2d55"}
  ]
}

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:

{
  "state": "applied",
  "detail_json": "{\"version\":1,\"kind\":\"build_progress\",\"phase\":\"rendering\",\"completed\":1,\"total\":2,\"jobs\":8}"
}

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 -d trixie -r pigsty

sow log export

Writes terminal Operations as JSONL — one complete Operation detail record per line — for archival or ingestion into a log pipeline.

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
exported 12 operations to /srv/audit/pigsty-ops.jsonl

Omit FILE, or pass -, to write to stdout:

sow log export - -r pigsty | gzip > pigsty-ops-$(date +%F).jsonl.gz
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:

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
operation rejected: export target already exists: /srv/audit/pigsty-ops.jsonl

export also refuses a target whose parent is not a real directory — a symlink, or a directory that does not exist:

sow log export /tmp/pigsty-ops.jsonl -r pigsty
log export parent is not a real directory

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.

sow log prune 2027-01-01 -r pigsty
{"operation":"8150803833883584722","repository":"pigsty","before":"2027-01-01T00:00:00+08:00","pruned":1}

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.

sow log prune yesterday -r pigsty
usage error: BEFORE must be YYYY-MM-DD or an RFC 3339 timestamp with 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:

sow log -r pgsql --json | jq -r '.result.operations[0] | "\(.id)\t\(.kind)\t\(.state)"'

List everything that failed:

sow log -r pgsql --json | jq -r '.result.operations[] | select(.state=="failed") | "\(.id)\t\(.error_class)\t\(.error_message)"'

Archive and shrink, monthly:

sow log export /srv/audit/pgsql-$(date +%Y%m).jsonl -r pgsql
sow log prune 2026-05-01 -r pgsql

Which Operation last touched a Dist:

sow log -d el9 -r pgsql --json | jq -r '.result.operations[0].id'

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