Skip to content

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

Return to the regular view of this page.

Commands

SOW command syntax, options, selection rules, and exit codes.

Use these pages for command syntax, selection rules, results, and examples. Run sow help COMMAND or sow help COMMAND SUBCOMMAND to check the options in your installed version.

Command index

sow create builds a flat repository directly in a directory. To manage packages in a Workspace, start with sow init. All other Managed commands use an existing Workspace. help and version can run anywhere.

Command Mode Purpose
sow create [DIR] Plain Generate a flat RPM/DEB repository in place
sow init [DIR] Managed Initialize a Workspace and create declared Repositories/Dists
sow config check|show Managed Validate or print the effective configuration
sow repo ls|new|show|migrate|rm Managed Create, inspect, upgrade, or remove Repositories
sow dist ls|new|show|rm Managed Manage Dists
sow add PATH... Managed Add packages and rebuild indexes
sow rm PACKAGE... Managed Remove packages and rebuild indexes
sow ls Managed List Desired packages with their Built locations
sow show PACKAGE Managed Inspect one package
sow where PACKAGE Managed Find a package across the Workspace
sow status Managed Show Repository state and pending changes
sow build Managed Build a Generation from Desired state
sow check Managed Verify a Repository before delivery
sow changes [BASE_GENERATION] Managed Compare files in two Built Generations
sow publish TARGET Managed Publish a verified Generation to a configured target
sow retain add|ls|rm Managed Protect a Generation’s package files from local GC
sow gc [TARGET] Managed Reclaim unused local packages or maintain a publication target
sow export rpm-leaf Managed Export a standalone RPM repository
sow log [OPERATION] Managed Read, export, and prune operation records

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 version and build information.

Options apply only to the commands that list them. Use -- before a positional argument that starts with -. Unknown or repeated single-value options return exit code 2.

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 use these defaults:

  • 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

create locks its target directory. Managed mutations coordinate through Workspace and Repository locks. init waits for its locks without a CLI timeout option.

Other write commands accept -T/--timeout DUR and -N/--no-wait. The default --timeout 0 waits indefinitely. Positive durations use ms, s, m, or h, for example 500ms, 30s, or 5m. --no-wait fails immediately if a lock is held. It cannot be combined with a positive timeout. Lock acquisition failure returns 4.

Most read-only Managed commands use shared locks and wait for a conflicting writer. They do not accept lock wait options. status normally probes the Repository lock so you can inspect an active writer. Cancel a waiting command with Ctrl-C.

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.

Use text output for interactive work and --json for scripts. config show prints YAML by default. log export is the exception: it writes JSONL directly and does not accept --json.

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 Rejected operation: conflict, protected object, no match, or incompatible architecture
130 Command interrupted by SIGINT or SIGTERM

See Exit Codes for command-specific triggers.

1 - sow create

Generate RPM and DEB indexes beside package files.

sow create writes RPM and DEB indexes beside the package files in a directory. Use it for a flat repository that does not need a managed Workspace. DIR defaults to the current directory.

Synopsis

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

Description

create reads the top-level regular package files in DIR. For RPMs, it generates repodata/. For DEBs, it generates Packages and Packages.gz. A mixed directory gets both formats. Architectures come from the package headers; Plain mode accepts all architectures found in the packages.

Indexes use relative package paths: the filename for RPM, and ./<filename> for DEB. Serve the package directory through HTTP or use it as a local file:// source.

By default, create updates its indexes and preserves the package files. --pigsty can delete matching packages, and --sign-with can replace RPMs with signed copies.

Options

Flag Description Default
-j, --jobs N Parallel package-processing workers logical CPU count
--metadata-timestamp SECONDS Unix seconds from 0 to 253402300799 for RPM repomd data timestamps; leaves DEB, package times and gzip headers unchanged 0
--pigsty Enable Pigsty compatibility cleanup and completion marker off
-S, --sign-with KEY Sign unsigned RPMs with a GPG key ID or fingerprint 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 Output JSON 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 package set so rerunning an interrupted cleanup can finish 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

Keep the input directory unchanged while create runs. SOW scans the packages, then checks for file changes before replacing the indexes. A detected change returns exit code 5. Use --jobs to set the number of parallel package-processing workers.

Deterministic output and idempotence

Without forced package re-signing, the rendered metadata is byte-stable for a given input set and timestamp: gzip output is deterministic. repomd.xml carries <revision>0</revision> and data timestamps defaulting to 0; --metadata-timestamp overrides the latter. Repeating the same options 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

If the directory contains repo_complete, use --pigsty to refresh it with the indexes. Running without --pigsty returns an error:

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

To keep using the completion marker, rerun with --pigsty. If you no longer use it, move the marker out of the directory first.

Publication time for existing YUM repositories

--metadata-timestamp sets the RPM repomd data timestamps. It does not change package files, compressed XML streams, or DEB indexes. The default is 0, and the accepted range is 0..253402300799 Unix seconds. This option was added in SOW 0.5.0.

EL7 YUM rejects metadata whose maximum data timestamp is older than its cached copy. When replacing an existing metadata generator, choose a timestamp at least as large as the highest value previously published. For changed content, advance it by at least one second to distinguish updates.

sow create /srv/flat --metadata-timestamp "${PUBLISH_TIME:?Set the publication time}"

Record the publication time and pass it on every update. Omitting the option resets it to 0. Restoring older package content is a new publication, so keep the timestamp increasing. Use seconds, not milliseconds, and avoid future dates.

If clients verify repomd.xml.asc, regenerate that signature after repomd changes. create neither signs repomd nor updates or removes its old detached signature.

See Migrating an Existing YUM Repository for the complete procedure.

–pigsty

--pigsty applies these cleanup rules and writes a completion marker:

  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 applies only to top-level regular package files that parsed successfully and matched a rule.

SOW removes the existing repo_complete before replacing indexes. It then deletes the packages matched by the cleanup rules and writes the new marker last. A missing marker means the build is incomplete.

Signing RPMs

-S/--sign-with KEY signs RPM package files through the local RPM/GPG environment. Use a 16-digit hexadecimal key ID or a 40- or 64-digit fingerprint. An optional 0x or 0X prefix is accepted. Configure the private key and passphrase in that environment before running the command.

  • 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.
  • SOW signs a temporary copy, checks that package identity and content are unchanged, then replaces the original RPM. The indexes describe the final signed file.
  • 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 locks the target directory while it updates the indexes. Use --timeout or --no-wait to control lock waiting.

An interruption can leave only part of the index set updated. Rerun sow create with the same intended options to rebuild from the current packages. Plain mode rebuilds from scratch; the result field recovered remains false.

For --pigsty, expose the repository only after repo_complete appears. Use Managed mode when you need journaled recovery.

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":[]}

Build a Pigsty flat repository with eight workers and a publication time chosen from its history:

sow create /www/pigsty -j 8 --pigsty --metadata-timestamp "${PUBLISH_TIME:?Set the verified publication time}"

The caller records this time and signs the resulting repomd.xml when clients require it.

Failure envelope:

sow create /srv/empty --json
{"schema":"sow.cli/v1","command":"create","ok":false,"repository":null,"operation":null,"result":null,"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 — invalid or repeated timestamp, --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

Initialize a Workspace and the Repositories and Dists declared in sow.yml.

sow init creates a Workspace with sow.yml and private .sow/ state. If the configuration already declares Repositories and Dists, it creates the missing ones. Run it again to complete initialization after adding declarations.

Synopsis

sow init [DIR] [--json]

DIR defaults to the current directory. To initialize another directory, pass its path as DIR.

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/ stores private state, locks, and recovery records with directory permissions 0700. Serve the Repository directories over HTTP; keep .sow/ private.

Options

Flag Description Default
--json Output JSON false
-h, --help Show help —

Idempotence rules

A new configuration uses schema: sow/v3 and architectures x86_64 and aarch64. It declares no Repository. Add one with sow repo new or edit sow.yml first.

init preserves an existing configuration and initializes missing declared objects. It can use a non-empty directory if no file conflicts with a reserved SOW path. When all declared objects are already initialized, the counters are zero.

Apply 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 valid empty indexes. An RPM Dist gets empty repodata/ in each architecture view. A DEB Dist gets empty Packages and Packages.gz files, by-hash entries, and a Release file.

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

If initialization is interrupted, rerun sow init for the same directory. SOW first completes or rolls back the interrupted operation, then initializes any remaining declared objects. It waits for other Workspace writers to finish.

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

Initialize 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 initialized, or already initialized (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 and inspect the effective configuration.

Use sow config check after editing sow.yml. It validates the configuration, signing references, and compatibility with existing Repository state. Use sow config show to inspect resolved defaults and inherited settings. Both commands are read-only.

Synopsis

sow config check [-C DIR] [--json]
sow config show [--all] [-C DIR] [-r NAME] [-d NAME]... [--json]

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 Output JSON false
-h, --help Show help —

Strict field rejection

Unknown keys produce a configuration error:

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

Use schema: sow/v3:

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

config check also verifies that the signing keys are available and suitable for signing. Before removing an architecture from the Workspace list, remove its use from Dist configuration, membership, and Built state.

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 use canonical family names: x86_64 and aarch64. The corresponding DEB names are amd64 and arm64.

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 Output JSON false
-h, --help Show help —

Select configuration 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: []

Without --all, normal Repository selection applies. If the discovery start directory is inside a Dist, that Dist is selected unless you provide -d. Otherwise, all Dists of the selected Repository are shown. --all shows the entire Workspace and ignores -r and -d.

Signing references

The output contains key references and resolved fingerprints. Private key contents and passphrases are omitted.

Examples

Validate before a build in CI:

sow config check -C /srv/repo || exit 1
sow build -C /srv/repo -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 or key-resolution failure
2 Usage or discovery error, invalid YAML, unknown field, or unsupported schema
5 Existing state, controlled paths, or recovery records are inconsistent
6 Unknown selection, unusable signing configuration, or configuration incompatible with existing state

config check validates both the YAML file and its compatibility with existing Repository state.

See also

4 - sow repo

Create, list, inspect, migrate, and remove Repositories.

sow repo creates, lists, inspects, upgrades, and removes Repositories. Each Repository stores its packages in pool/ and its indexes in dists/. Packages are shared among Dists in the same Repository.

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._-]*

SOW creates each Repository 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 Output JSON false

STATUS is one of clean, dirty, recovering or error. See Transactions & Recovery for what each one implies for clients.

sow repo new

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 Output JSON false

Pass the new Repository name as NAME.

Running it again on an existing Repository reports the current state without creating another Repository.

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 Output JSON false

If you give both NAME and -r, use the same name:

sow repo show demo -r empty
operation rejected: repo show NAME "demo" and --repo "empty" select different repositories

sow repo migrate

Use repo migrate when upgrading an existing v0.3 or v0.4 Workspace to SOW 0.5. New 0.5 Repositories already use schema v13 and need no migration.

  1. Stop Workspace writers.
  2. Back up the whole Workspace, preserving hard links.
  3. Migrate each configured Repository.
  4. Run build and check for each Repository.
rsync -aH /srv/sow/ /srv/sow.backup-before-0.5.0/
sow repo migrate pigsty -C /srv/sow
sow build -r pigsty -C /srv/sow
sow check -r pigsty -C /srv/sow

Repeat the last three commands for each Repository. The migration reports schema_from and schema_to; a repeated migration reports 13 -> 13. Schema v13 adds pool-path lookup indexes. Migration from v0.3 also applies the intervening state and publication repairs.

Writes require the current schema. Compatible read-only commands can inspect older state without upgrading it. After migration, use SOW 0.5 or later; restore the backup to return to the older schema.

--abort abandons an unfinished layout transition before its commit decision. It does not undo a completed schema migration. Pre-release C2 layouts must be rebuilt from their original packages.

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 Output JSON false

sow repo rm

Removes a Repository, including its sow.yml entry, database, package files in pool/, indexes in dists/, and private state.

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 Output JSON false

Force removal

Use -f to remove a non-empty Repository. A protected Repository remains protected when you use -f.

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, set its protected value to false in sow.yml. Run sow config check, then retry the removal.

protected applies only to Repository deletion. add, rm, build, and dist rm 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

Create a Repository without waiting for a held lock:

sow repo new nightly -N

Print each Repository’s name, state, and Generation:

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

Create, list, inspect, and remove RPM or DEB distributions.

sow dist manages the distributions that clients use. Each Dist contains either RPM or DEB packages. Dists in one Repository share pool/ and have separate indexes under dists/.

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.

Choose names that identify your distributions or channels, such as el9, trixie, or el9-beta. SOW does not infer package format or policy from the name.

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 Output JSON false

The table shows architecture families. JSON also shows the package ecosystem names. This excerpt contains the architectures field from one entry in result.dists; the DEB view directories are binary-amd64 and binary-arm64:

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

sow dist new

Create a Dist with --format rpm or --format deb.

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 Output JSON false

--format is required and accepts only rpm or deb:

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

A new Dist inherits the Workspace architectures. Edit sow.yml to select a subset or set limit and exclude, then run sow config check and sow build.

If the name and format already exist, dist new reports the current state. Using the same name 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

Creation and recovery

dist new updates sow.yml and builds the new Dist. If it is interrupted, run sow build to recover the operation.

The new Dist has valid empty indexes. An RPM Dist gets empty repodata/ in each architecture view. A DEB Dist gets empty Packages and Packages.gz files, by-hash/SHA256/ entries, and a Release file. When signing is configured, it also gets InRelease and Release.gpg.

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 Output JSON false

JSON also includes effective_config_sha256, the digest of the resolved Dist configuration. Changing limit, exclude, or a signing key can make the current configuration differ from the Built Generation. SOW then marks the Dist dirty until it is rebuilt.

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 Output JSON false

Dist removal does not delete Pool bytes

SOW removes the Dist’s configuration, membership, and indexes. Package files remain in pool/:

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

Run sow gc to reclaim package files when no current or retained state needs them.

If a configured target has published this Dist, removal is rejected. See Removing a published view.

A Repository’s protected: true blocks Repository deletion only; normal Dist maintenance on a protected Repository continues to work.

Examples

Create an RPM Dist and a DEB Dist in one Repository:

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

Create a beta Dist, set its package-retention policy in sow.yml, then rebuild it:

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

List Dists that need a build:

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 reported 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 Workspace or 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

Import packages, apply membership policy, and build local indexes.

sow add imports packages into the selected Dists and builds their local indexes. It reads package format and architecture from the package headers, then applies each Dist’s policy. Use --skip to import several batches before one build. To update a remote target, run sow publish after the build.

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 Output JSON false

Inputs and targets

PATH can be a file or a directory. A directory is scanned top level only unless you pass -R.

Select one Repository and one or more Dists. Use -r and -d, or the directory-based defaults. In a mixed RPM/DEB batch, each package goes only to Dists of its format. A package with no compatible target fails.

Choose Dists that match the package’s intended distribution. SOW checks package format and architecture; it does not test operating-system or dependency compatibility.

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 reports the Operation ID, item counts, and membership changes. It also shows the Desired Revision, Built Generation, and dirty state. Each input then has one item line, 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 repeated add also builds the selected Dist by default. If it is already current, the Generation stays unchanged. If an earlier --skip or configuration change left it dirty, SOW completes the 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 to postpone the build.

Package architecture

add reads format and native architecture from the package header, then checks the Workspace permit list. An unsupported architecture or one outside the configured list rejects that package:

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"

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. Only the selected Dists receive them.

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, canonical 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.

To restore a package after relaxing exclude or limit, run sow add again. Policy applies to Desired membership, not every file left in pool/.

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 every input fails, the operation exits 6; Desired memberships and public content remain unchanged. Human output includes each failed input and its reason, and JSON includes them in result.items:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm -r pigsty -d el9

Policy-excluded inputs are not input failures; an entirely excluded batch can succeed even when the Dist was empty. If you interrupt the command with SIGINT or SIGTERM, it returns 130. Rerun it with the original input files; recovery preserves committed membership changes.

For changed package content, use a new version and filename. A previously published pool path remains reserved after local GC. A path conflict rejects that input; other valid inputs can still succeed.

Keep filename capitalization consistent when adding the same package again. Paths that differ only by case conflict. On a case-insensitive filesystem, source-package directory names must also be distinct; see Workspace filesystem.

–skip

--skip commits Desired membership and leaves pool/, dists/, and the Built Generation unchanged. New package files stay in private pending storage until a build needs them. The Repository is dirty when the resulting Desired state differs from Built state; reusing an unchanged member can leave it clean.

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

SOW inspects the packages and signs new RPMs when configured. It applies exclude and limit, records the resulting membership, then builds unless --skip is set.

Input files remain unchanged. Signing uses private copies.

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.

Repeated imports reuse identical package content. They can also reuse an RPM whose only difference is its signature, if the stored package satisfies the current signing policy. A changed payload or an incompatible stored signature causes a coordinate conflict. See Signing Model for the matching rules.

Exit codes

Code Trigger
0 Inputs processed, including policy-excluded inputs; build completed or skipped
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 Every input failed, or selection/signing/state preconditions reject the operation

See also

7 - sow rm

Remove packages from selected Dists and preview the result.

sow rm removes packages from the selected Dists and rebuilds their local indexes. Use --check to preview the removal, or --skip to postpone the build. Package files remain in pool/ until sow gc can reclaim them.

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 Output JSON 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 matches every version and architecture of that name in the selected Dists. Other reference forms must identify one object. An ambiguous reference returns an error with the matching candidates.

sow ls prints exact sha256: references and canonical coordinates, so you never have to assemble one by hand.

If no package matches a reference, the command returns an error:

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

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

Preview with –check

-c/--check previews the removal and resulting local file changes without modifying the Repository. removed lists memberships matched by your references. If the current policy removes additional members, their index changes appear in changes; their membership count appears in sow log after the removal runs.

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

Both centos-release versions matched the bare name. The change lines describe the expected local file changes in payload → metadata → pointer → delete order. Use --json to read the removed[] and changes[] arrays in a script.

Preview validates the candidate configuration and existing state. It uses shared locks and can wait for a writer, but does not accept --timeout or --no-wait:

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

–skip

--skip commits the Desired change and leaves the public tree unchanged. The Repository is dirty if the resulting Desired state differs from Built state. Removing a pending-only addition can return it to clean without a build.

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 build the pending changes.

Policy interaction

After removal, SOW applies policy to the remaining members. If you remove libpq5 18.3-1 from a limit: 1 Dist, the previously excluded 18.2-1 stays excluded. Run sow add to restore it.

Examples

Preview a removal, then apply it:

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

Remove one exact object from two Dists with one command:

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
4 Repository lock held and --no-wait given or --timeout expired
5 Integrity or recovery error
6 A reference matched nothing, or a non-bare reference was ambiguous; the whole batch is rejected and no memberships are removed

See also

8 - sow ls

List Desired packages and their Built locations.

sow ls lists packages in the selected Dists’ Desired membership. Each row also shows the package’s Desired and Built locations across the Repository. Use it to obtain package references for show or rm.

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 Output JSON false

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 Repository
BUILT_DISTS Membership present in the current Built Generation
POOL_PATH Repository-relative immutable payload path

The first line reports whether the selected Dists are dirty. Configuration changes can make them dirty even when membership counts match.

-d selects rows by Desired membership. The DISTS and BUILT_DISTS columns then show all locations of each returned package in the Repository. A package removed with rm --skip is absent from this list even while an older Built index still contains it. An empty BUILT_DISTS means no local Built Dist currently includes that package.

Each package appears once, with multiple Dists separated by commas. An empty Dist produces a successful result with no package rows.

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 error: repository "pigsty" has multiple Dists (el9, trixie); select one or more with --dist

With --json, the same rows appear 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:" + .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 and its storage and membership.

sow show displays one package in the selected Repository. The text output summarizes its identity, storage, and membership. Add --json for the complete stored package record.

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 Output JSON 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.package 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 placed in the local package pool
dists / built_dists Desired and current Built Membership

Without -d, lookup includes stored objects with no current membership. With -d, candidates must be Desired members of a selected Dist. The returned dists and built_dists lists cover the entire Repository.

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

Find a package across the Workspace.

sow where finds the Desired membership locations of one package across a Workspace. It searches all Repositories by default. Use -r or -d to limit the search.

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 Output JSON false

Reference resolution

PACKAGE uses the same grammar as sow show: SHA-256 identity, canonical RPM/DEB coordinate, full filename, or bare name.

The reference must identify one package: the same format, coordinate, and SHA-256. That package can exist in several Repositories. If different packages match, use -r or -d to narrow the search, 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 Desired dists and current built_dists across its Repository. Candidate lookup uses Desired membership. A package removed from every selected Dist is no longer found, even if a previous Built index still contains it. Use sow show PACKAGE -r NAME without -d to inspect such a stored object.

With --json, the locations appear in result.locations. If no package matches, the command returns an error:

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

Example

List the Dists that include this package in Desired state:

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 state, pending data, and lock activity.

sow status reports whether a Repository needs a build, how much pending data it holds, and whether a writer is active. It reads stored state without hashing package files. Use sow check to verify file contents.

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 Output JSON false

Repository states

SOW records the intended package set as Desired state and the last build as a Built Generation.

State Meaning Next step
clean Desired state matches Built state Run check before copying the Repository
dirty Desired and Built state differ, commonly after --skip or a config change Run build to apply the changes
recovering A local Repository operation was interrupted Run build to recover it
error State, files, or recovery records need attention Read dirty_reasons, then run check and log

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 summarizes the recorded state. When it is false, dirty_reasons explains why. sow check performs the file-content validation.

Reading stored state

status reports stored state without migrating or repairing it. It can read database schemas v6, v12, and the current v13. Unsupported or unreadable state returns 5. Before writing to an older Workspace, back it up and run sow repo migrate for each Repository.

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

Build local indexes from Desired membership and current configuration.

sow build updates local indexes to match Desired membership and current configuration. Run it after add --skip, rm --skip, or a policy change. It recovers unfinished local Repository operations before starting the next build.

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 Output JSON false

Without -d, SOW builds every affected Dist in the selected Repository. With -d, only those Dists are built; 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 to read the result in a script.

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

Apply policy changes

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 moves required pending packages into pool/, writes the indexes, then updates client entry points such as repomd.xml and Release. One build can include several Dists. They share the resulting Generation; unselected dirty Dists still need a build.

If a build stops, rerun it. SOW uses the operation journal to finish or roll back the unfinished work. If the journal, database, and files disagree, the command reports an integrity error. Use sow check and sow log to identify the affected state.

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.

Inspect the events with sow log OPERATION.

Metadata signing

Configure metadata signing in sow.yml. 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 Build completed 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

Verify repository files, indexes, state, and signatures before delivery.

sow check verifies a managed Repository before delivery. It checks package hashes, stored state, indexes, signatures, and the Generation manifest. The command is read-only; run sow build first if Desired state differs from the Built Generation.

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 Output JSON false

Verification layers

For a Repository with no unfinished layout migration, check reports nine 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 Retained records and their saved Generation manifests; see retain rm to handle a damaged record 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 an unfinished layout migration, check reports config, state, public-modes, and layout-transition, then stops. Follow the reported repo migrate instruction to complete or abort that migration.

Package verification

Each run hashes the package files again. Hard links to the same physical file share one hash check. Signed RPMs also undergo signature verification. The checked count refers to package objects, not file reads.

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 needs a build before it can pass check. The old Built Generation and new Desired state can each pass their own checks while differing from each other:

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, then run sow check again.

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

Compare the files in two Built Generations.

sow changes lists file differences between a base Generation and the current Built Generation. Paths are relative to the Repository root. Build first to include pending Desired changes.

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 Output JSON 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

Use this plan to inspect the local file delta. To deliver it, use sow publish or stage a complete copy and switch it into service.

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, follow the diagnostic to recover or repair it before running changes.

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 a Built Generation to a filesystem or R2 target.

sow publish copies the current Built Generation to a target in the targets: section of sow.yml. The target selects the Repository and storage provider. Build first when the target must include the latest Desired changes.

Synopsis

sow publish TARGET [--abort | --rebind] [-C|--workdir DIR] [-T|--timeout DUR | -N|--no-wait] [--json]
Flag Meaning Default
--abort Abandon an interrupted publication before its commit phase 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 Output JSON false

TARGET must name a configured filesystem or r2 publication target. --abort and --rebind are mutually exclusive.

Before publication

Run build first to include the latest package and policy changes. A dirty Repository can still publish its previous completed Built Generation.

Configure the target’s web server or bucket access so that public_endpoint serves the published files. Then run sow publish TARGET.

Publication protocol

SOW verifies the local files against the completed Generation manifest, then publishes in this order:

  1. package files;
  2. metadata with content-specific URLs;
  3. client entry points, such as repomd.xml and Release;
  4. public visibility checks and a saved publication checkpoint.

SOW records progress so the same command can resume an interrupted publication. When the target already contains the current Generation, repeating the command changes nothing.

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 publication records the Repository and target binding. After changing the target name, public endpoint, or cache TTL, use --rebind to record the updated settings and continue publication:

sow publish prod --rebind

If you renamed the target in targets:, use its new name.

May change Immutable; configure a new target
target name Repository identity
public_endpoint provider, storage endpoint, or region
max_cache_ttl bucket or prefix

Complete pending target maintenance before changing max_cache_ttl. For a filesystem target, also complete pending deletion before changing public_endpoint. To publish to a new target for the first time, use sow publish TARGET without --rebind.

Abort and recovery

After an interruption, rerun sow publish TARGET to resume the publication.

To abandon the attempt, use sow publish TARGET --abort before it reaches commit_intent. SOW checks the objects already uploaded and records the abandonment. It leaves remote files in place. Once the attempt reaches commit_intent, rerun the ordinary publish command to finish it.

A published package URL identifies fixed content. Use a new package version and filename for changed content. If existing remote files differ from the recorded attempt, follow the reported conflict diagnosis; see Troubleshooting.

Public visibility checks

Before recording a checkpoint, SOW verifies that the published files can be read through public_endpoint. HTTP(S) verification uses ordinary GET requests. Stale or missing content is retried up to max_cache_ttl.

Filesystem targets support file:// or HTTP(S) public endpoints. R2 targets require HTTP(S). Each R2 publication lists the target prefix and checks each object with HEAD; an update performs this inventory check twice. Keep old objects on R2: target GC reports them without deleting them, and later publication checks the prefix against the recorded inventory.

Removing a published view

While a target remains configured, its published entry points must remain available locally. These include Dists, RPM architecture views, and signing companion files.

To retire that target, stop publishing to it, remove its entry from targets: in sow.yml, and run sow config check. You can then remove the local view. This leaves the old remote files and publication history intact. To publish the reduced repository, configure a new target with a different storage prefix and update clients to use it.

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

Keep or release the package files needed by a Generation.

sow retain protects the package files needed by a selected Generation from local GC. Add the current Built Generation before a later build replaces it. Retention preserves package files and a frozen manifest; it does not restore or publish an older repository view.

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

Pass the current Built Generation number. SOW verifies the Generation, saves its manifest, and records that GC must keep its package files. Add this record before building a newer Generation.

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

Adding the same Generation again changes nothing if its retained record still passes verification.

retain ls

Lists retained records for the Repository.

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 the retention record for a Generation:

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 can reclaim these packages when no other state or operation needs them.

If a retained record is damaged, retain rm, retain ls, local gc, and check report the error. The retained layer in check identifies the affected Generation. To discard that damaged record:

  1. Stop Workspace writers.
  2. Move .sow/<repo>/retained/<GENERATION> to a backup location outside the Workspace.
  3. Run sow check again.

Moving the record also removes its protection against GC.

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 Output JSON

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

Reclaim unused local packages or maintain a publication target.

Run sow gc to reclaim local package files that are no longer needed. Run sow gc TARGET to maintain a configured publication target. Filesystem targets can delete eligible objects; R2 targets only report candidates.

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 Output JSON 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

Run local GC after the Repository has a completed, clean build. If it is dirty, run sow build first. GC deletes a pool package only when none of these records references it:

  • current Desired or Built membership, or prior Built membership kept for older clients;
  • explicit retain records;
  • recovery and non-terminal Operation state;
  • publication records;
  • 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 GC waits for the configured cache grace period and checks which objects are still needed:

Provider Behavior
filesystem After the grace period, conditionally delete eligible objects, verify that their public URLs are absent, and record the result
r2 Save a report of eligible objects; leave the objects in storage
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.

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 RPM architecture view as a standalone repository.

sow export rpm-leaf creates a standalone RPM repository for one Dist and architecture. It copies the packages into an external directory and rewrites metadata to use local pool/... paths.

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 read-only output on the same filesystem copy files
-C, --workdir DIR Workspace discovery start directory current directory
-r, --repo NAME Select a Repository selection rules
--json Output JSON false

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:

  • RPM metadata with local package paths;
  • the required package subtree;
  • an export manifest;
  • .sow-export.json provenance.

The source Repository must be clean, have a completed Built Generation, and have no unfinished operation. The requested RPM view must belong to that Generation. The export is independent of the Workspace and does not protect source packages from GC.

If export fails, correct the reported cause and retry into a new or empty directory. The failed destination can contain partial files.

Managed RPM and export rpm-leaf use a fixed 0 repomd data timestamp and expose no --metadata-timestamp option. They cannot directly take over the same EL7 repository ID while clients retain a positive cached timestamp; changing the base URL alone does not help. Use a new client repository ID or clear its metadata cache. To keep the old ID and cache, continue with Plain create and an explicit publication time, following the migration guide and re-signing repomd.

Copying is the default. Use --hardlink only for read-only output on the same filesystem. Editing a hard-linked package also changes the source package in SOW’s pool.

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

Inspect, export, and prune operation records.

sow log lists Repository operations and shows the details of one operation. Use log export to archive completed records as JSONL. Use log prune to remove eligible old records from the database.

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]

sow log

With no argument, prints the 50 most recent Operations, newest first.

sow log -r pigsty --json

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 Output JSON false

One Operation in detail

Pass an Operation ID for a summary. Add --json to read the full transitions, timing, package, membership, and file records under result.detail.

sow log 4262183287563704350 -r pigsty --json

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. Use them to follow long builds.

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

Operation lifecycle

The state field shows how far an operation has progressed:

planned → staged → applied → built → done
                        └────────→ done_dirty
   any nonterminal → recovering → built / done / done_dirty / rolled_back / failed
   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 Operation ended with changes that still need a build, usually after --skip
failed Terminal — the operation failed before applied, nothing was committed
rolled_back Terminal — recovery abandoned the operation before it changed Desired state, for example a planned operation left by an interrupted command
recovering Non-terminal; the next write command must complete or roll it back

The Repository log includes dist new and dist rm. Workspace operations (init, repo new, repo rm) use separate recovery records and do not appear here.

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.

Output path

Choose a new filename in an existing directory. log export returns 6 if the file already exists. Its immediate parent must be a real directory, not a symbolic link. On macOS, use /private/tmp/file.jsonl instead of /tmp/file.jsonl.

sow log prune

Deletes eligible completed operation records older than BEFORE and reclaims database space. If another reader is using the database, record deletion can finish while space reclamation waits. The result then reports compaction_deferred: true. Close the reader and rerun prune to reclaim the space.

sow log prune 2027-01-01 -r pigsty
pruned log repository=pigsty operation=8150803833883584722 before=2026-12-31T16:00:00.000000000Z operations=1

The output shows the cutoff in UTC (this example uses a workstation at UTC+8). When a reader defers space reclamation, a second line reads log deletion committed; space reclamation deferred while the database is busy; with --json the journal ID and compaction_deferred appear in result.

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 Output JSON false

prune applies to the whole Repository and does not accept -d.

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

Records kept by prune

prune keeps:

  • a non-terminal Operation;
  • a record still required for recovery;
  • current Package or Membership state;
  • a Built Generation or its Changeset.

The pruned counter reports the number of deleted operation records.

Examples

Investigate the most recent write:

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

Find failed operations among the 50 most recent records:

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

Archive records, then remove those older than a chosen date:

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, or several different -d values were given
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