Skip to content

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

Return to the regular view of this page.

SOW Docs

Create and manage RPM/YUM and DEB/APT repositories with one self-contained binary.

SOW creates RPM/YUM and DEB/APT software repositories. Run sow create DIR to index packages in a directory. Use a Managed workspace when you need to organize packages into Dists, limit versions, sign metadata, or publish updates.

These docs cover SOW 0.6.1. See the release notes for version changes and the upgrade guide for an existing Managed workspace.

  • Get Started — Install SOW, create a flat repository, and build the first Managed workspace.
  • Tutorials — Complete YUM, APT, signing, serving, and publication walkthroughs.
  • Features — How package storage, filtering, signing, and recovery work.
  • Design Records — The reasons behind the architecture and its history.
  • Commands — Options, examples, output, and exit codes for every command.
  • Reference — Configuration fields, package names, directory layouts, and compatibility.

Choose a path

Goal Start here
Index a package directory now Quick Start
Replace an existing createrepo_c workflow Migrate an Existing YUM Repository
Maintain a curated long-lived repository First Workspace
Build a complete YUM or APT repository Tutorials
Look up exact CLI behavior Commands
Resolve an error or interrupted command Troubleshooting
Check a field, path, or compatibility claim Reference
Understand an architecture decision Design Records

1 - Get Started

Install SOW, create a flat repository, and learn the Managed workspace model.

SOW turns RPM and DEB files into static YUM and APT repositories. Install SOW, then choose how to manage your packages:

  • Plain mode: run sow create DIR to build indexes for the packages in a directory.

  • Managed mode: create a workspace to group packages into Dists, limit versions, sign metadata, and publish updates.

  • Installation — Install a release archive, Linux package, or source build.

  • Quick Start — Build a flat repository and use it from a local client.

  • First Workspace — Create RPM and DEB Dists, add packages, and check the result.

  • Core Concepts — Learn how workspaces, repositories, Dists, and generations fit together.

Serve the generated files through a static HTTP server or object storage. See Installation for storage and signing requirements.

1.1 - Installation

Install SOW from an archive, RPM/DEB package, or source, then verify the binary and filesystem requirements.

SOW is one executable. It needs no daemon, database service, or language runtime. Release archives support Linux and macOS on amd64 and arm64. Linux also has RPM and DEB packages.

Use the Download page to select a SOW 0.6.1 artifact. Download it and SHA256SUMS into a separate download directory.

Install an archive

For Linux amd64, verify the archive, extract it, and install the executable:

set -euo pipefail
grep '  sow_0.6.1_linux_amd64.tar.gz$' SHA256SUMS | sha256sum -c -
tar -xzf sow_0.6.1_linux_amd64.tar.gz
sudo install -m 0755 sow /usr/local/bin/sow

For macOS, select darwin_amd64 or darwin_arm64. Substitute that archive name and use shasum -a 256 -c - for the checksum command.

To install without root access:

mkdir -p "$HOME/.local/bin"
install -m 0755 sow "$HOME/.local/bin/sow"
export PATH="$HOME/.local/bin:$PATH"

Add the export PATH line to your shell profile to keep it in new sessions.

Install a Linux package

Verify the downloaded package against its entry in SHA256SUMS. Then run the command for your distribution:

# RPM, x86_64
sudo rpm -Uvh ./sow-0.6.1-1PGSTY.x86_64.rpm

# Debian or Ubuntu, amd64
sudo apt install ./sow_0.6.1-1PGSTY_amd64.deb

For ARM64, use the RPM ending in .aarch64.rpm or the DEB ending in _arm64.deb. Packages install the executable at /usr/bin/sow.

Build from source

Install Go 1.27.3 or newer and Git. The build uses CGO_ENABLED=0 and needs no C compiler.

set -euo pipefail
git clone --branch v0.6.1 https://github.com/pgsty/sow.git
cd sow
SOW_BUILD_DIR="$(mktemp -d)"
CGO_ENABLED=0 go build -trimpath \
  -ldflags='-s -w -X github.com/pgsty/sow/internal/v2cli.Version=0.6.1' \
  -o "$SOW_BUILD_DIR/sow" ./cmd/sow
sudo install -m 0755 "$SOW_BUILD_DIR/sow" /usr/local/bin/sow

Verify the installation

sow version
sow help

version shows the product version, target operating system and architecture, and Go toolchain. help lists commands. Use sow help COMMAND for a command’s options.

Upgrade to 0.6.1

SOW 0.6.1 keeps database schema v13, configuration schema sow/v3, and the public directory layout. Workspaces already on 0.5.0 or 0.6.0 need no schema migration.

  1. Stop all workspace writers.
  2. Back up the complete workspace with a tool that preserves hard links, such as rsync -aH or tar.
  3. Install SOW 0.6.1.
  4. Build and check each Repository in sow.yml:
sow version
sow build -r REPOSITORY -C /srv/sow
sow check -r REPOSITORY -C /srv/sow

Replace REPOSITORY with its configured name. A current repository produces a no-op build. An unchanged build keeps its Generation and metadata timestamps. New builds record one publication time and reuse it during recovery.

Workspaces from 0.3 or 0.4

These workspaces need migration before the first build. After backing up the workspace and installing 0.6.1, run:

sow repo migrate REPOSITORY -C /srv/sow
sow build -r REPOSITORY -C /srv/sow
sow check -r REPOSITORY -C /srv/sow

Repeat these commands for each Repository. Migration updates the database; the rebuild applies the current RPM verification and APT metadata rules. Keep the backup for rollback to a version that uses an older schema. See sow repo migrate and the 0.6.0 release notes.

Upgrade a Plain repository

Plain mode has no database to migrate. For an existing YUM repository, follow Migrate an Existing YUM Repository before changing its generator. The --metadata-timestamp default remains 0. Set the timestamp from the repository’s publication history so cached YUM clients accept the new index.

Storage and optional tools

The SOW user needs read access to input packages and write access to the repository. Keep Managed workspaces on a local POSIX filesystem with file locks, fsync, and atomic rename.

Package parsing and metadata generation run inside SOW. Optional signing needs these host tools:

Signing method Required tools
Metadata signing with an OpenPGP key file or key material None
Metadata signing with a GPG key identifier gpg and gpg-agent
RPM package signing rpm with a working GPG signing setup

Continue with Quick Start for Plain mode or First Workspace for Managed mode.

1.2 - Quick Start

Index a directory of RPM and DEB packages, serve it, and configure a client.

Use Plain mode to build a repository from a directory of packages. It works without a workspace or configuration file. You need SOW, Python 3 for the preview, and at least one RPM or DEB file.

Prepare a directory

Create a writable directory. Replace the input paths below and run the copy command for each package format you have:

mkdir -p "$HOME/sow-quickstart"
cp /path/to/example.rpm "$HOME/sow-quickstart/"
cp /path/to/example.deb "$HOME/sow-quickstart/"

Keep packages at the directory top level; sow create does not scan subdirectories.

Generate metadata

sow create "$HOME/sow-quickstart"

With one RPM and one DEB, the output includes:

rpm=1 deb=1 signed=0 removed=0 marker=false noop=false recovered=false

The directory now has indexes beside the original packages:

sow-quickstart/
├── example.rpm
├── example.deb
├── repodata/       # RPM index and metadata
├── Packages        # DEB flat index
└── Packages.gz

SOW creates indexes for the formats present. Plain mode does not create APT Release, InRelease, or Release.gpg files. It leaves package names and paths unchanged.

Start a local server

Run this command in a separate terminal and leave it running:

python3 -m http.server --bind 127.0.0.1 --directory "$HOME/sow-quickstart" 8080

If this host has no APT or DNF client, check the HTTP entry point for the format you added:

# RPM
curl --fail http://127.0.0.1:8080/repodata/repomd.xml >/dev/null

# DEB
curl --fail http://127.0.0.1:8080/Packages.gz >/dev/null

This server is reachable only from the same host. For other clients or a persistent service, follow Serve Repositories.

Configure a local client

On a Linux host running the preview server, create the source file for your package manager. These examples use unsigned metadata and disable client signature checks. For signed metadata, use a Managed workspace and the signing tutorial.

Save this as /etc/yum.repos.d/sow-quickstart.repo:

[sow-quickstart]
name=SOW Quick Start
baseurl=http://127.0.0.1:8080/
enabled=1
gpgcheck=0
repo_gpgcheck=0

Refresh the index and install one of your packages:

sudo dnf --disablerepo='*' --enablerepo=sow-quickstart makecache
sudo dnf install PACKAGE_NAME

Save this as /etc/apt/sources.list.d/sow-quickstart.list:

deb [trusted=yes] http://127.0.0.1:8080/ ./

The final ./ selects a flat repository. Refresh the index and install one of your packages:

sudo apt update
sudo apt install PACKAGE_NAME

Replace PACKAGE_NAME with a package name from its metadata, not its filename. On macOS, use the HTTP checks above; run package installation on a compatible Linux client.

Update the repository

Add or remove package files, then regenerate the indexes:

sow create "$HOME/sow-quickstart"

Unchanged package files and options produce the same metadata. A repeat run reports noop=true.

When replacing another YUM metadata generator, follow Migrate an Existing YUM Repository to preserve the timestamp that cached clients expect.

When the preview is complete, stop the server with Ctrl+C and remove its source file from your client. Keep the package directory if you want to reuse it.

Next

Use a Managed workspace for named Dists, architecture views, version limits, signed metadata, and publication targets. See sow create for all Plain options.

1.3 - Your First Workspace

Create a workspace, organize RPM and DEB packages, and check the repository.

This example creates a Managed repository with two named package sets, called Dists: el9 for RPMs and bookworm for DEBs. Use an empty, writable /srv/sow directory, or substitute another path.

Initialize the workspace

sow init /srv/sow
cd /srv/sow

init creates:

/srv/sow/
├── sow.yml   # configuration; schema: sow/v3
└── .sow/     # private state managed by SOW

Edit sow.yml when you need to change configuration. SOW manages the private .sow/ directory. You can repeat init without resetting existing packages or state.

Create a Repository and two Dists

sow repo new local
sow dist new el9 --format rpm
sow dist new bookworm --format deb

Each Dist has one package format. dist new creates empty indexes for x86_64 and aarch64 by default. Both Dists share the Repository’s pool/ directory.

The public layout is now:

/srv/sow/local/
├── pool/
└── dists/
    ├── el9/
    │   ├── x86_64/repodata/
    │   └── aarch64/repodata/
    └── bookworm/
        ├── Release
        └── main/
            ├── binary-amd64/{Packages,Packages.gz,by-hash/}
            └── binary-arm64/{Packages,Packages.gz,by-hash/}

Add packages

Select the target Dist explicitly:

sow add /path/to/packages/*.rpm -d el9
sow add /path/to/packages/*.deb -d bookworm

Replace the input paths with real package files. SOW reads package identity and architecture, stores each accepted file in local/pool/, updates membership, and builds the affected Dists. Later builds use the pool, so the original input files are no longer needed by SOW.

Inspect and verify

sow status
sow ls -d el9
sow ls -d bookworm
sow check

ls lists the packages in each Dist. status should report clean and ready_to_copy=true. A successful check confirms that the files match the recorded build.

Serve the Repository

Publish the complete /srv/sow/local directory so clients can reach both indexes and package files. Keep the workspace configuration and .sow/ outside the server root.

Follow Serve Repositories to publish with Nginx. The YUM and APT tutorials show client configuration, including the trust settings for unsigned repositories.

Batch later updates

When you need to add several batches, use --skip and build once at the end:

sow add /path/to/more/*.rpm -d el9 --skip
sow build

While membership changes await a build, sow status reports dirty and ready_to_copy=false.

Selection rules

The commands above find the workspace from the current directory and select its only Repository. From another directory, use -C /srv/sow. With multiple Repositories, add -r local. Use -d NAME to select a Dist; repeat it to select several.

See selection rules for discovery and defaults.

Next

1.4 - Core Concepts

The SOW model: Plain and Managed execution, pools and views, Desired Membership, and Built Generations.

Choose a mode

Need Plain Managed
Build indexes from a package directory sow create DIR Import packages with sow add
Keep configuration and package membership Directory contents are the state sow.yml and private SQLite databases
Organize packages One flat directory; RPM and DEB can coexist Named Dists; one format per Dist
Publish architecture views No Yes
Apply filters and version limits Pigsty conventions with --pigsty Configurable per Dist
Sign RPM packages Yes Yes
Sign repository metadata Sign separately Configure keys in sow.yml
Track generations and publication Manage these outside SOW Built in

Use Plain when the directory already contains the packages you want to publish. Use Managed when SOW should track membership and maintain the repository over time.

Workspace, Repository, and Dist

Workspace                    /srv/sow
├── sow.yml                  configuration
├── .sow/                    private state
└── Repository               /srv/sow/local
    ├── pool/                package files
    └── dists/
        └── Dist             a named RPM or DEB package set
            └── views        metadata for each architecture

A workspace holds configuration and private state for one or more repositories.

A Repository owns a package pool, Dists, build history, and publication targets. Serve its complete pool/ and dists/ tree. Keep sow.yml and .sow/ outside the HTTP document root.

A Dist selects packages of one format. For example, el9 can contain RPMs and bookworm can contain DEBs. You choose the names; SOW does not check whether a package is compatible with the operating system named by its Dist.

An architecture view contains metadata for one architecture. SOW includes neutral RPM noarch and DEB all packages in each applicable view.

Store each package once

SOW identifies a package object by the SHA-256 of the complete package file. It reads the package name, version, and architecture from package metadata.

Within a Repository, each accepted package has one path under pool/. Multiple Dists can refer to that path. Separate repositories have separate pools.

RPM metadata uses relative paths back to the pool. APT Packages entries use paths relative to the Repository root. Keep the complete Repository tree together when you serve or copy it.

Some mirror tools, including default dnf reposync, reject the parent-relative RPM paths. Use sow export rpm-leaf to create a standalone RPM repository for those tools.

Desired membership and built generations

Desired membership is the package set selected for each Dist. A Generation identifies a completed build and its file manifest. Built Generation is the current completed build. Package files stay in the shared pool; each build does not create another copy.

add / rm → Desired membership → build → Built Generation

add and rm normally update membership and build the affected Dists in one command. With --skip, they update membership only. Run sow build after a batch of skipped changes, or after changing build or signing configuration.

Command Use
sow status Read the current state and ready_to_copy value
sow check Check package files, indexes, signatures, and recorded state without changing them
sow changes [BASE_GENERATION] List file changes from a recorded generation to the current build
sow publish TARGET Check and publish the current build to a configured target

Understand repository state

State Meaning Next action
clean Membership and configuration match the built output Ready for check or publication
dirty Membership or configuration needs a build Run build, then check
recovering An interrupted operation needs recovery Follow the error and recovery steps
error Stored state or files conflict Inspect the reported error and operation log

Use sow log to inspect failed or interrupted operations.

Continue

2 - Tutorials

Build, sign, serve, and update RPM and DEB repositories.

These tutorials cover repository creation, signing, publication, and updates. Start with Installation if you have not installed SOW.

Create RPM architecture views, filter packages, set version limits, and configure DNF.

Use both package formats and architectures, test installation, and maintain rolling channels, stable releases, and monthly snapshots.

Follow the examples

Run commands in order. Replace uppercase placeholders and /path/to/ inputs with your values. Examples under /srv require a writable directory; you can substitute a path under your home directory.

The YUM and APT tutorials each start with a new workspace. To combine them, create the second Dist in the existing pigsty Repository and keep both Dist definitions in sow.yml.

Each tutorial shows how to check its result. For a command error or interrupted operation, see Troubleshooting.

2.1 - Build a YUM Repository

Create a managed RPM repository, apply membership policy, serve it, and configure dnf.

This tutorial creates a new Managed RPM repository. Install SOW and Python 3, and prepare RPM files built for your clients. Use an empty /srv/sow directory that your user can create and write to, or substitute another writable path.

1. Create the workspace

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new el9 --format rpm -r pigsty

el9 names this RPM package set. You can choose another name; it does not change which operating systems the packages support.

2. Set membership policy

Edit the generated sow.yml. This example keeps one version per package and architecture and excludes debug and LLVM JIT packages:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]
targets: {}

Check the configuration:

sow config check

exclude runs before limit. SOW includes noarch packages in every enabled architecture view. Keep only CPU architecture names in architectures.

3. Add RPMs

sow add /path/to/packages/*.rpm -r pigsty -d el9
sow check -r pigsty

Replace the input path with your RPM directory. add stores accepted packages in the pool, updates membership, and builds the indexes. Review its per-package output: excluded files are reported without failing the whole command.

The public tree has this shape:

/srv/sow/pigsty/
├── pool/...
└── dists/el9/
    ├── x86_64/repodata/...
    └── aarch64/repodata/...

RPM indexes point back to pool/ through relative paths. Serve or copy the complete pigsty/ tree so clients can download the packages.

4. Preview over HTTP

Run this in a separate terminal and leave it running:

python3 -m http.server --bind 127.0.0.1 --directory /srv/sow/pigsty 8080

The preview serves only the Repository root and is reachable from the same host. For a persistent service or clients on other hosts, use Serve Repositories.

5. Configure dnf

On the Linux host running the preview, save this as /etc/yum.repos.d/pigsty.repo. For a remote client, first publish the Repository and substitute its public base URL:

# /etc/yum.repos.d/pigsty.repo
[pigsty-el9]
name=Pigsty EL9
baseurl=http://127.0.0.1:8080/dists/el9/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0

Then refresh and query the repository:

sudo dnf --disablerepo='*' --enablerepo=pigsty-el9 makecache --refresh
dnf --disablerepo='*' --enablerepo=pigsty-el9 list available

This example uses unsigned packages and metadata. Follow Sign Your Repository to enable verification. After the preview, remove this source file and stop the Python server.

6. Publish or export

Use sow publish for a filesystem or R2 target. It uploads package files and metadata in the order clients need. That guide also covers manual delivery to isolated hosts.

Some mirroring tools, including default dnf reposync, reject rpm-md package locations that traverse to the root pool. Export a self-contained RPM leaf when such a consumer is required:

sow export rpm-leaf el9 x86_64 /srv/export/pigsty-el9-x86_64 -r pigsty

The RPM Dist must belong to the current Built Generation. In a mixed Repository, check the export preconditions first.

The destination must be absent or empty. Export copies package files by default. Use --hardlink only on the same filesystem, and keep exported files read-only because they share storage with the source pool.

Update the repository

sow add /path/to/new.rpm -r pigsty -d el9
sow check -r pigsty

add builds the update automatically. With limit: 1, the newest version replaces the previous member for each package and architecture. Run sow build -r pigsty after changing configuration.

To withdraw a package, preview the selection with sow rm PACKAGE_NAME -r pigsty -d el9 --check, then repeat without --check. A package name selects all its versions and architectures; use a SHA-256 reference for one package object.

See Platforms & Integrations for tested clients.

2.2 - Migrate an Existing YUM Repository

Move a flat RPM repository to SOW while preserving client cache compatibility, package trust, and publication time.

This guide covers a flat directory of RPMs and repodata/, such as a repository previously built with createrepo_c. It uses Plain sow create, without creating a Managed workspace. The commands below target SOW 0.6.1.

1. Prepare a working copy

Back up the current packages and metadata. Make a separate working copy for the migration; copy RPM files without hard links if you plan to re-sign them. Check the installed binary:

SOW="$(command -v sow)"
"$SOW" version

Confirm that the version is 0.6.1.

Check Action
Package layout Plain reads top-level regular RPMs; migrate nested package layouts separately
Client features Plain emits primary/filelists/other XML, not SQLite databases or module streams
Package trust Keep valid upstream signatures, or deliberately replace them with your own key
Index trust If clients use repo_gpgcheck=1, sign and verify the new repomd.xml separately
Existing clients Include clients with cached metadata, using their existing URL and repository ID

If your clients use modular filtering, keep the repository’s existing module_hotfixes=1 setting where needed. Plain metadata does not include module streams or profiles.

2. Preserve publication history

EL7 YUM can reject an index whose largest data timestamp is older than the cached index. Read <data><timestamp> in the current repomd.xml and retained copies, then use the largest value clients may have seen. If the live index was reset to zero, recover that value from earlier copies or publication records. Use data timestamps, not revision or file modification times.

Have the maintenance script serialize updates and save the greatest published timestamp outside the served tree. For changed content, choose:

next time = max(current Unix second, largest known publication time + 1)

Save the chosen timestamp before switching the public index. A failed publication may leave a gap in the sequence. Keep this record when you restore old content or move the repository to another directory. The +1 distinguishes publications; EL7 also accepts equal timestamps.

For unchanged content, reuse the timestamp after checking the indexes, referenced checksums, and signature. Repair a zero or regressed timestamp. Passing the current time on every run changes repomd.xml, so create can no longer report a no-op.

3. Build and sign a candidate

For this one-off example, prepare /srv/yum.candidate as an independent copy. Set HISTORICAL_MAX to the verified maximum from the live index, the saved maximum publication time, and trusted history. Set SIGN_KEY to the full fingerprint of your metadata signing key. The following commands use Python 3 to choose a strictly newer time:

set -euo pipefail
: "${SOW:?Set the path to the tested SOW binary}"
: "${HISTORICAL_MAX:?Set the verified historical maximum Unix second}"
: "${SIGN_KEY:?Set the full metadata signing fingerprint}"
PUBLISH_TIME=$(python3 -c 'import sys,time; h=int(sys.argv[1]); assert 0 <= h < 253402300799; print(max(int(time.time()), h+1))' "$HISTORICAL_MAX")

"$SOW" create /srv/yum.candidate --metadata-timestamp "$PUBLISH_TIME"
gpg --batch --yes --armor --detach-sign --local-user "$SIGN_KEY" \
  --output /srv/yum.candidate/repodata/repomd.xml.asc \
  /srv/yum.candidate/repodata/repomd.xml
gpg --verify /srv/yum.candidate/repodata/repomd.xml.asc \
  /srv/yum.candidate/repodata/repomd.xml

Record PUBLISH_TIME in the maintenance system before publishing the candidate. Compare the fingerprint from gpg --verify with the expected signing key.

--sign-with KEY signs unsigned RPMs and preserves existing signatures, including vendor signatures. To replace every package signature with your key, add --overwrite in the candidate copy. This changes package bytes; create builds indexes from the signed result. Generate the detached index signature after the final create run.

4. Test the candidate

Point a test client’s original repository URL at the candidate directory. Keep its old cache, repository ID, and signature settings. Refresh the index, then download and install a package. This checks cache compatibility, package paths, and signatures.

If you are migrating an automated update script, also check two cases:

  1. With the same inputs, options, and timestamp, a repeat build reports noop=true. Preserve the existing valid detached signature.
  2. After package content changes, the script advances the timestamp and the same client can fetch the new package.

When comparing with createrepo_c, check dependency meaning and client behavior; XML formatting can differ. See Platforms & Integrations.

5. Publish the repository

Plain replaces multiple files; it does not make the whole directory switch atomically. Coordinate package replacement, metadata, detached signatures, and cache visibility in your publication layer. SOW 0.6.1 retains the current and immediately previous SOW checksum-named RPM metadata sets. Unchanged runs keep the same previous set. Keep .sow-plain-rpm-history.json when copying a directory for further builds. For clients that cache indexes across several updates, retain the required older metadata and packages in your publication layer.

Keep the original backup and the saved maximum publication time. Roll back package content by rebuilding and signing it as a newer publication. Restoring an old repomd.xml beside changed RPMs can reintroduce checksum or timestamp failures.

Use the checks above for Plain repositories; sow check applies to Managed repositories. See sow create for options and Signing for package and metadata signatures.

2.3 - Build an APT Repository

Create a managed DEB repository with by-hash indexes and configure an APT client.

This tutorial creates a new Managed DEB repository. Install SOW and Python 3, and prepare DEB files built for your clients. Use an empty /srv/sow directory that your user can create and write to, or substitute another writable path.

1. Create the workspace

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new trixie --format deb -r pigsty

The Dist name trixie becomes the client’s Suites value. You can choose another name; it does not change which distributions the packages support.

2. Set membership policy

Edit the generated sow.yml if you need filtering or version limits:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      trixie:
        format: deb
        limit: 1
        exclude:
          - kind: [dbgsym, dbg]
targets: {}

Then validate it:

sow config check

Use x86_64 and aarch64 in configuration. They generate Debian indexes for amd64 and arm64. Architecture-independent all packages enter both views.

3. Add DEBs

sow add /path/to/packages/*.deb -r pigsty -d trixie
sow check -r pigsty

Replace the input path with your DEB directory. add stores accepted packages once and builds the indexes. The public tree is:

/srv/sow/pigsty/
├── pool/...
└── dists/trixie/
    ├── Release
    └── main/
        ├── binary-amd64/
        │   ├── Packages
        │   ├── Packages.gz
        │   └── by-hash/SHA256/...
        └── binary-arm64/...

Paths in Packages are relative to the Repository root. Serve the complete pigsty/ directory so APT can reach both the indexes and the package files.

4. Preview over HTTP

Run this in a separate terminal and leave it running:

python3 -m http.server --bind 127.0.0.1 --directory /srv/sow/pigsty 8080

The preview serves only the Repository root and is reachable from the same host. For a persistent service or clients on other hosts, use Serve Repositories.

5. Configure APT

On the Linux host running the preview, save this as /etc/apt/sources.list.d/pigsty.sources. Use arm64 in Architectures for ARM64 clients. For a remote client, first publish the Repository and substitute its public URL:

# /etc/apt/sources.list.d/pigsty.sources
Types: deb
URIs: http://127.0.0.1:8080
Suites: trixie
Components: main
Architectures: amd64
Trusted: yes

Then refresh and query it:

sudo apt update
apt-cache policy

The output should include trixie/main as a package source. Trusted: yes is used for this unsigned example. To add signatures, follow Sign Your Repository to sign metadata, install the public key, and configure Signed-By. After the preview, remove the test source file and stop the Python server.

6. Publish the repository

Use sow publish for a filesystem or R2 target. It uploads packages and indexes before updating Release and its signatures. That guide also covers manual delivery to isolated hosts.

Update the repository

sow add /path/to/new.deb -r pigsty -d trixie
sow check -r pigsty

add builds the update automatically. With limit: 1, the newest version replaces the previous member for each package and architecture. Run sow build -r pigsty after changing configuration.

To withdraw a package, preview the selection with sow rm PACKAGE_NAME -r pigsty -d trixie --check, then repeat without --check. A package name selects all its versions and architectures; use a SHA-256 reference for one package object.

See Platforms & Integrations for tested clients.

2.4 - Sign Your Repository

Sign RPM and APT metadata, optionally sign RPM packages, and enable client verification.

Repository signing has two layers: metadata signatures and RPM package signatures. Configure each layer separately:

Path Output Client control
RPM metadata repodata/repomd.xml.asc repo_gpgcheck=1
APT metadata InRelease and Release.gpg Signed-By
RPM package body embedded RPM signature gpgcheck=1

APT verifies package hashes through the signed Release file, so DEBs need no separate package signature. Start with metadata signing. Add RPM package signing when clients require gpgcheck=1.

This guide uses the pigsty Repository from the YUM or APT tutorial. Run only the commands for the formats and Dists you created.

1. Create a dedicated key

Install GPG first. These commands create an unencrypted test key and export it to a private directory. For an existing protected key, use a passphrase reference as described below.

SIGNING_UID='SOW Repository <[email protected]>'
gpg --batch --pinentry-mode loopback --passphrase '' \
  --quick-generate-key "$SIGNING_UID" rsa3072 sign 2y

FPR="$(gpg --batch --with-colons --list-secret-keys "$SIGNING_UID" \
  | awk -F: '$1 == "fpr" {print $10; exit}')"
test -n "$FPR"

sudo install -d -m 0700 /srv/sow-secrets
sudo chown "$(id -u):$(id -g)" /srv/sow-secrets
gpg --batch --pinentry-mode loopback --passphrase '' --armor \
  --export-secret-keys "$FPR" > /srv/sow-secrets/repo-signing.asc
gpg --armor --export "$FPR" > /srv/sow-secrets/repo-signing.pub
chmod 600 /srv/sow-secrets/repo-signing.asc

SOW reads the private key from /srv/sow-secrets, outside the public Repository. Give the SOW service account access to this directory if it differs from your user. Distribute repo-signing.pub to clients.

2. Configure metadata signing

In /srv/sow/sow.yml, merge these metadata entries into the Repository’s existing signing block. Keep its package policy and Dist definitions. Include only the formats you use:

repos:
  pigsty:
    signing:
      rpm:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
      deb:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
    dists:
      # existing Dist definitions remain here

Validate the configuration, rebuild, and check:

cd /srv/sow
sow config check
sow build -r pigsty
sow check -r pigsty

For a protected key, add passphrase: env://SOW_METADATA_PASSPHRASE next to key, then supply that environment variable to SOW. You can also use a passphrase file or agent://FINGERPRINT to sign through GPG. See passphrase references. Configuration stores references, not secret values.

3. Optional: inspect the metadata signer

The preceding sow check verifies metadata signatures. To inspect the signer with GPG, run the commands for your Dists:

gpg --verify \
  pigsty/dists/el9/x86_64/repodata/repomd.xml.asc \
  pigsty/dists/el9/x86_64/repodata/repomd.xml

gpg --verify pigsty/dists/trixie/InRelease
gpg --verify \
  pigsty/dists/trixie/Release.gpg \
  pigsty/dists/trixie/Release

Compare the displayed fingerprint with the public key you distribute to clients.

4. Optional: sign RPM package bodies

For a new repository, configure package signing before importing RPMs. Add this block under the existing signing.rpm configuration:

repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: fill
          key: agent://REPLACE_WITH_THE_FINGERPRINT
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc

Replace the placeholder with the 40-hex fingerprint in $FPR. Install rpm and gpg, and make the secret key available in the GPG keyring used by rpm.

fill signs newly imported packages unless their signatures already verify against the configured key or trusted_keys. See RPM signing policy for other modes.

SOW signs a private copy during add; the input file stays unchanged. build checks stored RPMs against the policy but does not re-sign them.

If you already imported RPMs, check their signatures before enabling fill. Keep trusted upstream public keys in trusted_keys. For unsigned or untrusted stored packages, prepare correctly signed packages with new versions and filenames. Alternatively, configure signing in a new repository, then import the originals.

After configuring the policy, import a new package and check the result:

sow config check
sow add /path/to/new.rpm -r pigsty -d el9
sow check -r pigsty

To inspect a resulting package independently, run rpmkeys --checksig on its file in pigsty/pool/.

5. Enable dnf verification

First publish the signed Repository. Then transfer the public key to the client through a trusted channel:

sudo install -m 0644 /path/to/repo-signing.pub /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

Then enable the checks that correspond to what you signed:

[pigsty-el9]
name=Pigsty EL9
baseurl=https://repo.example.com/pigsty/dists/el9/$basearch/
enabled=1
repo_gpgcheck=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

If you sign only metadata, use repo_gpgcheck=1 and gpgcheck=0. With package checking enabled, install each trusted upstream package key as well as your own key. Then run sudo dnf makecache --refresh and install a package.

6. Enable APT verification

After publishing the signed Repository, install its public key on the client as a dedicated keyring:

sudo gpg --dearmor --yes \
  --output /usr/share/keyrings/pigsty-archive-keyring.gpg /path/to/repo-signing.pub
sudo chmod 0644 /usr/share/keyrings/pigsty-archive-keyring.gpg

Reference it from deb822 configuration and do not set Trusted: yes:

Types: deb
URIs: https://repo.example.com/pigsty
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/pigsty-archive-keyring.gpg

Run sudo apt update and install a package. If verification fails, compare the client keyring fingerprint with the signer shown by gpg --verify. For ARM64 clients, change Architectures to arm64.

Plain-mode RPM signing

Plain mode can sign RPM package bodies, but it does not sign repository metadata or create an APT Release:

sow create /srv/flat --sign-with 0123456789ABCDEF

Replace 0123456789ABCDEF with your key ID or fingerprint. The matching private key must be available to rpm and GPG. Existing signatures, including upstream signatures, are preserved. To replace all RPM signatures with your key, use --overwrite on a separate working copy. See sow create for key formats.

For an existing YUM repository, follow the migration guide to preserve publication time and sign repomd.xml after the final create run.

Key changes

To rotate a metadata key, distribute the new public key, update the key reference, then run build, check, and publish. Changing the reference or resolved fingerprint marks affected Dists dirty.

For RPM package keys, use fill and keep the old public key in trusted_keys until packages signed by it are withdrawn or replaced. Keep the corresponding client key available during this period. Existing package objects remain unchanged.

Test the final repository with the DNF or APT versions your users run. See Platforms & Integrations for automated coverage.

2.5 - Serve and Publish Repositories

Serve a public Repository with Nginx and publish verified Generations to a filesystem target.

SOW generates static repository files. This guide publishes a Managed Repository to a separate directory, then serves that directory with Nginx. It continues from First Workspace.

The source is /srv/sow/local; the public copy will be /srv/repo-public/local. Both contain the complete pool/ and dists/ tree. The workspace configuration and private state stay outside the server root.

1. Build and check

cd /srv/sow
sow build -r local
sow check -r local

Continue when check succeeds.

2. Add a filesystem target

Create the target’s parent directory and give the SOW operator write access:

sudo install -d -m 0755 /srv/repo-public
sudo chown "$(id -u):$(id -g)" /srv/repo-public

Use the account that runs sow publish if it differs from your current user. The endpoint must already exist at its physical path; use no symlink components.

Add this target to /srv/sow/sow.yml:

targets:
  public:
    repository: local
    provider: filesystem
    endpoint: file:///srv/repo-public
    prefix: local
    public_endpoint: file:///srv/repo-public/local/
    max_cache_ttl: 0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true

SOW creates /srv/repo-public/local from endpoint and prefix. The three required booleans declare that this workspace is the source of truth and the only writer for that target. Use a separate prefix for other tools.

This example checks the published files through file:// and declares no intermediary cache. If you later add a proxy or CDN, use its public Repository URL for public_endpoint and its longest cache lifetime for max_cache_ttl. Apply these changes through publish --rebind. SOW uses them to check delivery and decide when old files can be removed.

Publish the Repository:

sow publish public

SOW copies package files and immutable metadata first, then updates client entry points. It checks the result and records the published generation. Repeating publication without changes reports a no-op.

3. Configure Nginx

Install Nginx and add a server configuration like this. Replace repo.example.com with your hostname:

server {
    listen 80;
    server_name repo.example.com;

    root /srv/repo-public;
    autoindex off;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ (^|/)\. {
        deny all;
    }
}

Check and reload the configuration:

sudo nginx -t
sudo nginx -s reload

These commands assume Nginx is already running. If this is a new installation, start it through your operating system’s service manager after nginx -t succeeds.

The client addresses are:

Client Address
DNF baseurl http://repo.example.com/local/dists/el9/$basearch/
APT URIs http://repo.example.com/local
APT Suites / Components bookworm / main

Use the YUM or APT tutorial for the full source configuration. To verify signatures, distribute the public key and follow Sign Your Repository.

4. Check client access

Configure a client with the addresses above. Refresh its indexes and install a package. This checks access to both dists/ and pool/. Apply the same access rules to both directories.

Publish an update

After changing membership or configuration, build any pending changes and publish again:

sow build -r local
sow check -r local
sow publish public

If publication stops, rerun sow publish public to resume it. See sow publish for recovery and target changes, and sow gc for cleanup after the cache grace period.

Deliver to an isolated host

When SOW cannot reach the destination, stop source writers while creating and checking the copy:

  1. Run sow check on the source.
  2. Copy the complete Repository to a new release directory on the destination. Preserve hard links, for example with rsync -aH or tar.
  3. Verify transferred files against sow changes 0 or an archive checksum manifest.
  4. Switch the server’s path to the new release directory.
  5. Keep package and immutable metadata URLs referenced by old indexes reachable until the maximum cache lifetime expires, then clean up the old release.

For paths absent from the new release, the HTTP server can fall back to the previous release directory during that interval.

Copying directly over a live tree can expose indexes before their package files arrive. Prepare the new directory first and switch it as a unit.

Serve a Plain repository

For Plain mode, serve the directory passed to sow create. Keep temporary hidden files inaccessible; the Nginx dotfile rule above does this.

Publish to R2

Use provider: r2 for Cloudflare R2. Configure its endpoint, bucket, credential reference, public URL, and cache lifetime; see the target configuration reference. Run an initial publication to a test prefix and check it from your package clients.

R2 target garbage collection reports deletion candidates; SOW does not delete those objects. See Platforms & Integrations for tested storage operations.

2.6 - Build the pigsty-infra Repository

Turn an existing dual-architecture RPM and DEB package pool into an infra repository, then validate installs, roll updates, promote to Stable, and take monthly snapshots.

pgsty/infra-pkg is the upstream build source for Pigsty Infra packages. This tutorial uses already-built RPMs and DEBs for x86-64 and ARM64. It creates an infra Repository with rolling channels, stable releases, and monthly snapshots. Install SOW, jq, and Docker first. The installation examples use pg-exporter; include that package and any dependencies not already in the client images.

1. Put the packages under ~/repo

Use a new ~/repo directory for this workspace. Copy packages from your infra-pkg checkout into two input directories:

mkdir -p ~/repo/packages/rpm ~/repo/packages/deb
cp ~/pgsty/infra-pkg/dist/rpm/*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/*.deb ~/repo/packages/deb/

Include both x86-64 and ARM64 packages. Step 3 checks their formats and architectures from package metadata.

2. Create the infra Repository and two Dists

Initialize a Workspace and create the Repository named infra:

sow init ~/repo
cd ~/repo
sow repo new infra
sow dist new rpm --format rpm -r infra
sow dist new deb --format deb -r infra

Set limit: 1 for both Dists in ~/repo/sow.yml:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  infra:
    dists:
      rpm:
        format: rpm
        limit: 1
      deb:
        format: deb
        limit: 1

limit: 1 keeps only the newest version for each package name and native architecture. The rpm and deb Dists are therefore rolling latest channels while still retaining both x86-64 and ARM64.

sow config check

3. Ingest once and build once

Import both package formats, then build their indexes together:

cd ~/repo
sow add ~/repo/packages/rpm --recursive -r infra -d rpm --skip
sow add ~/repo/packages/deb --recursive -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

After sow check succeeds, list the formats and architectures read from package metadata:

sow ls -r infra -d rpm -d deb --json |
  jq -r '.result.packages | group_by(.format + "/" + .canonical_arch)[] |
    "\(.[0].format)\t\(.[0].canonical_arch)\t\(length) packages"'

The output must include at least:

deb     aarch64   ... packages
deb     x86_64    ... packages
rpm     aarch64   ... packages
rpm     x86_64    ... packages

SOW reports canonical architecture names, so DEB amd64/arm64 appear here as x86_64/aarch64.

4. Read the generated filesystem tree

The important structure is:

~/repo/
├── sow.yml                            # configuration; never serve it
├── .sow/                              # database, locks, recovery; never serve it
├── packages/                          # original input pool; archive as desired
│   ├── rpm/
│   └── deb/
└── infra/                             # complete public Repository Root
    ├── pool/                          # one shared payload pool for RPM and DEB
    └── dists/
        ├── rpm/
        │   ├── x86_64/repodata/
        │   └── aarch64/repodata/
        └── deb/
            ├── Release
            └── main/
                ├── binary-amd64/
                └── binary-arm64/

SOW places all Dists under dists/ and packages under pool/. Publish or mount the complete ~/repo/infra tree so every Dist can reach its package files.

5. Serve the repository read-only with Nginx

Use the official nginx:alpine image and mount the Repository Root read-only at /infra:

docker network create --internal infra-lab
docker run --detach \
  --name infra-nginx \
  --network infra-lab \
  --publish 127.0.0.1:8080:80 \
  --volume "$HOME/repo/infra:/usr/share/nginx/html/infra:ro" \
  nginx:alpine

Nginx can see only ~/repo/infra; it cannot see sow.yml or .sow/, and it cannot modify the repository.

6. Install an RPM from only infra on EL9

The Rocky Linux 9 container below is attached to the --internal network. The script removes every preconfigured repository and enables only infra, so a successful installation cannot fall back to a public mirror.

docker run --rm --interactive --network infra-lab rockylinux:9 bash -s <<'ROCKY'
set -euxo pipefail

rm -f /etc/yum.repos.d/*.repo
cat >/etc/yum.repos.d/infra.repo <<'REPO'
[infra]
name=Pigsty Infra RPM
baseurl=http://infra-nginx/infra/dists/rpm/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0
REPO

dnf clean all
dnf --disablerepo='*' --enablerepo=infra makecache
dnf --disablerepo='*' --enablerepo=infra install -y pg-exporter
rpm -q --qf '%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n' pg-exporter
command -v pg_exporter
ROCKY

An RPM baseurl must point to a concrete architecture view. dnf expands $basearch to x86_64 or aarch64.

7. Install a DEB from only infra on Ubuntu 24.04

APT points URIs at the Repository Root and uses the Dist name deb as Suites:

docker run --rm --interactive --network infra-lab ubuntu:24.04 bash -s <<'UBUNTU'
set -euxo pipefail

rm -f /etc/apt/sources.list
rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
cat >/etc/apt/sources.list.d/infra.sources <<'SOURCE'
Types: deb
URIs: http://infra-nginx/infra
Suites: deb
Components: main
Trusted: yes
SOURCE

apt-get clean
apt-get update
apt-get install -y --no-install-recommends pg-exporter
dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n' pg-exporter
command -v pg_exporter
UBUNTU

The lab uses unsigned repositories. Follow Sign Your Repository to enable repo_gpgcheck=1 for RPM metadata and Signed-By for APT. Use gpgcheck=1 when RPM package signatures are also available. Remove Trusted: yes from the signed APT source.

Docker containers use the host architecture by default. To test the other architecture, add --platform linux/amd64 or --platform linux/arm64 to the corresponding docker run. Cross-architecture execution requires binfmt/QEMU support.

8. Routine maintenance: add a new version

To update the repository, run add with the new packages. Suppose the four new pg-exporter payloads are ready:

cp ~/pgsty/infra-pkg/dist/rpm/pg-exporter-*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/pg-exporter_*.deb ~/repo/packages/deb/

cd ~/repo
sow add ~/repo/packages/rpm/pg-exporter-*.rpm -r infra -d rpm --skip
sow add ~/repo/packages/deb/pg-exporter_*.deb -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

Because the latest Dists have limit: 1, the new version wins and the old version automatically leaves that Dist’s Desired Membership. The old bytes are not deleted immediately, and relaxing policy later does not make the old membership reappear automatically.

Rerun the EL9 and Ubuntu clients from sections 6 and 7, refresh metadata, then install or upgrade to complete the update acceptance test.

To withdraw a bad package, use sow ls -r infra -d rpm --json to find its SHA-256. Preview with sow rm sha256:DIGEST -r infra -d rpm --check, then repeat without --check. Use -d deb for DEBs. A SHA-256 reference selects one package object; a bare name selects all its versions and architectures. sow gc handles unused pool files separately.

9. Two retention layers: latest and stable

limit: 1 makes rpm and deb good rolling channels but cannot express “keep every formally released version.” Create two more Dists for that purpose:

cd ~/repo
sow dist new rpm-stable --format rpm -r infra
sow dist new deb-stable --format deb -r infra
sow config show -r infra -d rpm-stable -d deb-stable

The new Dists default to limit: 0, which means retain every version. The resulting policy is:

Dist Format limit Role
rpm RPM 1 RPM latest
deb DEB 1 DEB latest
rpm-stable RPM 0 Accumulate promoted RPMs
deb-stable DEB 0 Accumulate promoted DEBs

Stable accumulates the versions you explicitly promote after creating the Dists.

10. Promote latest into stable

Promote packages by adding the source Dist’s pool files to the target Dist. SOW verifies and reuses the existing package objects, so promotion adds no second copy. Pause other repository writers before exporting the manifests. Resume them after promotion finishes.

First require clean source state and save the promotion manifests:

set -euo pipefail
cd ~/repo
sow check -r infra
mkdir -p ~/repo/manifests

sow ls -r infra -d rpm --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-latest-202608.list
sow ls -r infra -d deb --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-latest-202608.list

Add the recorded pool objects to the stable Dists:

cd ~/repo
(
  set -euo pipefail
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-stable --skip
  done < ~/repo/manifests/rpm-latest-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-stable --skip
  done < ~/repo/manifests/deb-latest-202608.list

  sow build -r infra -d rpm-stable -d deb-stable
  sow check -r infra
)

Each add should report reused. If the loop stops midway, the source Dists are unchanged; correct the failure and rerun the same manifest. Over subsequent promotions, rpm/deb retain only the latest version while rpm-stable/deb-stable accumulate release history.

11. Take the 2026-08 snapshot from stable

A client-visible monthly snapshot is another pair of Dists:

cd ~/repo
sow dist new rpm-202608 --format rpm -r infra
sow dist new deb-202608 --format deb -r infra
sow config show -r infra -d rpm-202608 -d deb-202608

Pause other repository writers while creating the snapshot. First save the stable package lists:

set -euo pipefail
sow check -r infra
sow ls -r infra -d rpm-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-stable-202608.list
sow ls -r infra -d deb-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-stable-202608.list

Add those manifests to the corresponding snapshot Dists:

cd ~/repo
(
  set -euo pipefail
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-202608 --skip
  done < ~/repo/manifests/rpm-stable-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-202608 --skip
  done < ~/repo/manifests/deb-stable-202608.list

  sow build -r infra -d rpm-202608 -d deb-202608
  sow check -r infra
)

To keep this build after you eventually remove the snapshot Dists, also retain the current Repository Generation:

sow retain add "$(sow status -r infra --json | jq -r '.result.built_generation')" -r infra
sow retain ls -r infra

retain stores this build’s metadata and protects its package files from garbage collection. Clients still use the snapshot Dists. SOW does not make these Dists read-only; keep their membership and effective configuration unchanged to preserve their contents.

12. Client address map

One Nginx service and one shared infra/pool support every channel:

Channel dnf baseurl APT URIs / Suites
latest http://infra-nginx/infra/dists/rpm/$basearch/ http://infra-nginx/infra / deb
stable http://infra-nginx/infra/dists/rpm-stable/$basearch/ http://infra-nginx/infra / deb-stable
2026-08 http://infra-nginx/infra/dists/rpm-202608/$basearch/ http://infra-nginx/infra / deb-202608

Use sow dist ls -r infra to see all six Dists.

The rolling Dists keep the latest packages, stable Dists accumulate promoted releases, and monthly Dists keep the selected snapshot. All views share the same package pool.

Stop the temporary service when the lab is complete:

docker rm --force infra-nginx
docker network rm infra-lab

3 - Features

Choose a repository mode, then add package policies, signing, publication, and audit.

SOW creates repository indexes in two modes. Choose Plain for a directory you can rebuild. Choose Managed for ongoing package updates, policy, signing, and publication.

Capabilities

Capability Plain Managed
RPM and DEB indexes One directory, either or both formats One repository, separate Dists
Stored membership and generations — Yes
Per-architecture views — Yes, including neutral packages
Package exclusions Fixed cleanup with --pigsty Configurable per Dist
Version limits — Configurable per Dist
Metadata signing — RPM and DEB
RPM package signing --sign-with never, fill, always
Recovery after interruption Rerun create Recorded operations and publication attempts
Operation log and JSONL export — Yes
Publication targets — Filesystem and R2

SOW parses packages and generates indexes itself. It does not require createrepo_c, dpkg-scanpackages, or reprepro. RPM package signing uses the host rpm and GPG environment. Metadata signing uses gpg when a key reference resolves to a GPG key identifier.

Generated formats

Format Plain Managed additions
RPM primary, filelists, other XML and repomd.xml Architecture views; optional repomd.xml.asc
DEB Packages, Packages.gz Release, by-hash indexes; optional InRelease and Release.gpg

RPM XML is gzip-compressed. SOW does not generate SQLite rpm-md, zchunk, modulemd, or source-package indexes. Use your HTTP server or object-storage service to serve the output. See Platforms and integrations for client details.

Learn a feature

Task Read
Generate indexes beside existing packages Plain repositories
Manage repeated package updates Managed workspaces
Share package bytes across Dist views Pool and views
Exclude packages or keep recent versions Membership policy
Sign packages and indexes Signing
Continue after an interruption Transactions and recovery
Inspect state, validate output, and read history Observability and audit

3.1 - Plain Flat Repositories

Build flat RPM and DEB indexes, repeat interrupted builds, and use signing or the Pigsty marker.

Use sow create to build indexes for a directory of RPM or DEB packages. It writes the indexes beside the packages. It does not read sow.yml or create a state database.

sow create /srv/offline

Inputs and outputs

SOW scans top-level regular files with .rpm or .deb suffixes. It does not recurse or follow package symlinks. Package names, versions, and architectures come from package headers. Source RPMs (src and nosrc) are rejected.

Input Generated files
RPM repodata/repomd.xml and gzip-compressed primary, filelists, and other XML
DEB Packages and Packages.gz
Both formats Both sets of indexes

RPM locations use the package basename. DEB Filename values use ./<basename>. All valid versions are included. Different bytes with the same package coordinate cause a conflict. A directory with no supported packages is rejected, except when --pigsty finishes an interrupted cleanup.

By default, package files and unrelated files stay unchanged. If a format disappears, SOW removes its old generated indexes. For RPMs, it keeps the current and immediately previous checksum-named metadata sets. It removes older SOW-shaped metadata and replaces incomplete output. Generated directories use 0755; generated files use 0644.

Build and repeat

The normal unsigned build reads each selected package once to calculate SHA-256 and parse its metadata. --jobs controls this work and defaults to the logical CPU count. SOW renders and validates the indexes from those parsed facts.

Before replacing the indexes, SOW checks the package list, file identity, mode, size, and modification time again. Keep other programs from changing packages during the build. A changed snapshot stops publication with exit code 5.

Identical package bytes and options produce identical metadata. If the generated files already match, the command reports noop=true in text output or noop: true in JSON and leaves them in place.

RPM metadata uses revision 0 and data timestamp 0 by default. Set a publication time when updating an existing repository used by EL7 YUM:

sow create /srv/offline --metadata-timestamp 1791849600

The timestamp changes repomd.xml only. It does not change the compressed XML, package bytes, or DEB indexes. Maintain a timestamp that advances from the existing repository; see YUM migration.

A small .sow-plain-rpm-history.json file tracks RPM metadata retention. Unchanged builds keep the same previous index. Keep this file when copying a directory that you will rebuild. Package retention remains the operator’s responsibility. See RPM index retention.

Interrupted builds

SOW stages and checks metadata before it replaces public files. The replacements do not form one transaction across all RPM and DEB files. An interruption can leave mixed or incomplete indexes.

Run the same sow create command again. It removes its stale temporary files and rebuilds from the packages currently in the directory. Plain mode has no operation journal; the JSON field recovered is always false.

For a live service, build in a separate directory and switch the complete directory into service after the command succeeds. Use Managed mode when you need recorded membership, generations, and transaction recovery.

Pigsty cleanup and completion marker

sow create /srv/offline --pigsty

--pigsty removes DEB i386 packages and Patroni 3.0.4 packages according to parsed package facts. It does not remove an RPM solely because its architecture is i386, i486, i586, or i686.

Documentation packages such as annobin-docs are retained unless they match those cleanup rules.

After updating indexes and removing the selected packages, SOW writes repo_complete. Each line contains a SHA-256, two spaces, and a retained package basename. Lines are sorted by basename.

SOW removes an existing marker before replacing public files and writes the new marker last. Consumers that use this convention must wait for the marker. If the build stops without a marker, rerun with --pigsty. A plain invocation without --pigsty refuses to rebuild a directory that already contains repo_complete.

RPM package signing

sow create /srv/offline --sign-with 6D5C5A26C36B1F73

This option signs RPMs that have no parseable embedded signature. Add --overwrite to re-sign every retained RPM. It requires the host rpm command and a usable GPG key. SOW signs and checks staged copies, then replaces the package files before their indexes. This path reads package bytes more than once.

Plain mode does not sign repository metadata. See Signing for Managed metadata signing and package-signing policy.

Concurrent commands

sow create locks the target directory and its parent. Use -N to fail immediately if another SOW writer holds the lock, or -T 30s to wait up to 30 seconds. The default waits until the lock becomes available.

See sow create for all options and result fields.

3.2 - Managed Workspaces

Workspace structure, package membership, configuration, discovery, and initialization.

Use Managed mode for repositories that receive regular updates. SOW records package membership, applies Dist policy, builds metadata, and keeps operation history.

Workspace, repository, and Dist

Workspace
└── Repository
    └── Dist
        └── Architecture view
Term Purpose
Workspace Holds sow.yml, private .sow/ state, and the allowed architecture families
Repository Owns one package pool, Dists, a SQLite database, generations, and publication state
Dist Selects a named set of packages in one format: rpm or deb
Architecture view Presents a Dist’s packages to clients of one architecture

A repository can contain RPM and DEB Dists. Dist names such as el9, trixie, and el9-beta are labels you choose; they do not inspect operating-system compatibility or create a promotion workflow.

One package can belong to several Dists in the same repository without another payload copy. An architecture-neutral package has one membership per Dist and appears in each applicable architecture view. Separate repositories keep separate package objects.

Public files and private state

<workspace>/
├── sow.yml
├── .sow/
│   ├── workspace.lock
│   ├── workspace-ops/
│   ├── repo-locks/<repo>.lock
│   ├── <repo>.db
│   └── <repo>/
│       ├── stage/
│       ├── recovery/
│       ├── pending/
│       ├── retained/
│       └── transitions/
└── <repo>/
    ├── pool/
    └── dists/

Serve or publish the complete <workspace>/<repo>/ directory. Keep sow.yml and .sow/ private. Back up configuration, private state, and package files together when you need to preserve a working workspace. See Repository layout.

Create and configure

sow init /srv/repo
sow repo new demo -C /srv/repo
sow dist new el9 --format rpm -r demo -C /srv/repo
sow dist new trixie --format deb -r demo -C /srv/repo

The initial configuration uses schema: sow/v3 and architecture families [x86_64, aarch64]. init creates no repository unless one is already declared in the configuration. Repository and Dist lifecycle commands update sow.yml for you.

Edit sow.yml to set architecture restrictions, membership policy, signing, or publication targets. Then validate the configuration:

sow config check -C /srv/repo
sow config show --all -C /srv/repo

The parser rejects unknown fields. config check also compares configuration with initialized state and resolves signing references. config show --all expands defaults. Configuration accepts amd64 and arm64 as aliases for x86_64 and aarch64. DEB indexes use the native names amd64 and arm64.

To prevent removal of a repository, set protected: true on it. This blocks repo rm, including -f; package and Dist changes remain available.

Desired state and built state

Desired state records which packages should belong to each Dist. Built state records the last completed metadata generation.

By default, add and rm update membership and build the affected output. With --skip, they update membership without building. New package bytes stay in private pending/ until a build promotes them to the pool.

sow add ./incoming/ -C /srv/repo -r demo -d el9 --skip
sow status -C /srv/repo -r demo
sow build -C /srv/repo -r demo
sow check -C /srv/repo -r demo

A dirty repository needs a build. Its public tree still represents the previous built generation. Policy, architecture, or signing changes can also make a Dist dirty. Removing an architecture still referenced by membership or built state is rejected.

Every new Dist starts with valid empty indexes for its architectures. Metadata signing, when configured, applies to empty Dists too. Removing the last member leaves empty indexes.

Find the workspace

Managed commands search for the nearest ancestor sow.yml:

  1. Start at -C/--workdir DIR, if supplied; otherwise start at the current directory.
  2. If no workspace is found, search from $SOW_DIR.
  3. If neither search succeeds, report a discovery error.

The first configuration found is used, even if it contains an error. --workdir changes workspace discovery, not the process directory. Relative input paths still refer to your current directory:

sow add ./build/example.rpm -C /srv/repo -r demo -d el9

A symlink can be a search starting point, but the search follows the supplied path’s ancestors. A symlink within the workspace prevents implicit repository or Dist selection. Use the physical workspace path and explicit -r and -d when needed.

Plain sow create does not use workspace discovery.

Select a repository and Dist

Repository selection uses the first available choice:

  1. Explicit -r/--repo NAME.
  2. The repository containing the discovery starting directory.
  3. The only repository in the workspace.

add, rm, and ls select Dists in this order:

  1. Explicit -d options; repeat the option to select several Dists.
  2. The Dist containing the discovery starting directory.
  3. The only Dist in the selected repository.

If more than one choice remains, the command requires an explicit selection.

build, check, and status default to all Dists in the selected repository. show searches that repository; where searches all repositories. Use -d to narrow commands that accept it. See Commands for each command’s selection rules.

Repeat initialization or upgrade

Rerunning init fills in missing declared repositories and Dists. It checks existing initialized state without resetting generations. After adding an architecture to an existing Dist, run build to create its new view.

If initialization commits some objects and then fails, it returns exit 3 and reports what completed. Resolve the error and rerun init.

SOW 0.6.1 uses database schema v13, as do 0.5.0 and 0.6.0. Upgrading from either version needs no schema migration. For a 0.3 or 0.4 workspace, back up the complete workspace, then run sow repo migrate. Do not edit the SQLite schema version manually.

The database also caches parsed package facts. Warm builds can reuse those facts and file fingerprints; sow check reads and hashes package bytes for a full consistency check.

See Policy, Views, and Transactions and recovery for the next steps.

3.3 - Pool & Metadata Views

Share package files across RPM and APT views, export RPM leaves, and deliver complete repositories.

A Managed repository stores each package object once under pool/. Its Dists and architecture views contain indexes that refer to those package bytes.

<repo>/pool/                              package files
<repo>/dists/<rpm-dist>/<arch>/repodata/   RPM metadata
<repo>/dists/<deb-dist>/main/binary-*/     APT metadata

The storage boundary is one repository. The same package in another repository or publication target occupies that repository’s or target’s namespace.

RPM views

An RPM Dist has one metadata directory per configured architecture:

demo/
├── pool/
│   ├── e/example/example-1.0-1.x86_64.rpm
│   └── d/demo-data/demo-data-1.0-1.noarch.rpm
└── dists/el9/
    ├── x86_64/repodata/
    │   ├── <sha256>-primary.xml.gz
    │   ├── <sha256>-filelists.xml.gz
    │   ├── <sha256>-other.xml.gz
    │   └── repomd.xml
    └── aarch64/repodata/
        └── ...

The x86_64 view includes x86_64 and noarch packages. The aarch64 view includes aarch64 and noarch packages. Both views refer to the same noarch file.

A package location is relative to the architecture directory:

<location href="../../../pool/d/demo-data/demo-data-1.0-1.noarch.rpm"/>

Point the client at dists/el9/x86_64/, and serve the complete demo/ directory so that the relative package location resolves. sow check verifies that indexed locations stay inside the repository and resolve to the expected package objects.

APT views

APT indexes use paths relative to the repository root:

Filename: pool/e/example/example_1.0-1_amd64.deb

Each configured architecture has Packages, Packages.gz, and SHA-256 by-hash entries under dists/<dist>/main/binary-<arch>/. The Dist root contains Release and, when signed, InRelease and Release.gpg.

all packages appear in each applicable architecture index while their payload remains one pool object. There are no per-view package copies or per-architecture Release files.

Export for dnf reposync

Default dnf reposync rejects Managed RPM locations that climb above its per-repository download directory. Create a standalone leaf for that workflow:

The RPM Dist must belong to the current Built Generation. Check the export preconditions before running this command.

sow export rpm-leaf el9 x86_64 /srv/exports/el9-x86_64 -r demo

The export has local package paths, its own repodata, a manifest, and a .sow-export.json completion marker. Choose a destination outside the repository and all configured filesystem publication roots.

The default copies files. --hardlink requires the same filesystem and makes exported packages share the source inode; keep those files read-only. An export does not create Dist membership or a retained generation and is not input to sow publish.

Deliver the complete repository

Use a configured publication target for live updates. For another transport, copy the complete settled pool/ + dists/ tree into a separate staging directory, verify the copy, then switch it into service. Keep package URLs referenced by cached indexes reachable until those caches expire. See Serving for a complete transfer procedure. Copying only an RPM architecture directory leaves its package locations unresolved.

sow changes 0 lists each package under pool/ once, followed by metadata and client pointers. It lists local files; sow publish manages target progress and cache handling.

See Repository layout, Serving, and export.

3.4 - Membership Policy

Configure exclusions and version limits, read policy results, and restore removed members.

Use Dist policy to exclude unwanted packages and limit the number of versions in indexes. Set exclude and limit in sow.yml:

dists:
  el9:
    format: rpm
    limit: 2
    exclude:
      - kind: [debuginfo, debugsource, llvmjit]

SOW applies exclude first, then limit. add, rm, and build apply policy to the complete candidate membership of each selected Dist. Edit the policy, run sow config check, then run sow build to apply it to existing members.

Exclude packages

Each rule contains one or more fields. All fields in a rule must match. Any pattern within a field can match. A package is excluded when any rule matches.

exclude:
  - kind: [debuginfo, debugsource, dbgsym, dbg, llvmjit]
  - name: ["test-*", "*-experimental"]
    arch: [aarch64]

This excludes the listed package kinds on every architecture. It also excludes aarch64 packages whose names start with test- or end with -experimental.

Field Package value
name Binary package name
source Normalized source package name
arch x86_64, aarch64, or neutral
kind Classification in the table below
format rpm or deb

Patterns are case-sensitive exact strings or shell globs: *, ?, and [...]. Regular expressions, version comparisons, and negation are not supported. Empty rules, empty or untrimmed patterns, duplicate patterns within a field, and invalid globs are errors.

SOW derives kind from the binary package name:

Format Name suffix kind
RPM -debuginfo debuginfo
RPM -debugsource debugsource
RPM -llvmjit llvmjit
DEB -dbgsym dbgsym
DEB -dbg dbg
Either No matching suffix main

Use sow show PACKAGE --json to inspect the computed kind. Pattern values are not checked against this table: kind: [debug] is valid YAML but matches no package kind.

Limit versions

limit groups packages by binary name and canonical architecture family. Neutral packages (noarch or all) form their own group.

Value Result
0 Keep all versions; this is the default
Positive N Keep the newest N versions in each group
Negative value Configuration error

RPM versions use epoch-version-release ordering. DEB versions use Debian version ordering. Versions are not sorted as plain strings.

With limit: 2, adding version 3 to a Dist that contains versions 1 and 2 removes version 1 from membership in the same operation. An x86_64 package and a noarch package count in separate groups, even when they share a binary name.

Read policy results

An input excluded from every selected Dist has item status excluded. Its per-Dist result is excluded for an exclusion rule or limited for the version cap. Policy exclusion does not count as a failed input and does not make the command fail.

A package can be accepted by one Dist and excluded by another. If no Dist accepts a new package, SOW does not store an unreferenced package object for it.

Restore a removed version

Relaxing policy does not restore previous members. Raising limit, removing an exclusion, or deleting the newest version only changes the rules for current and future candidates. To restore a version, add its package file again:

sow add ./archive/example-1.0-1.x86_64.rpm -r demo -d el9

Policy removes membership, not package bytes. A package can remain in the pool because a built or retained generation still needs it. Use sow gc to reclaim unreferenced objects. Use sow gc TARGET for a published target.

Preview removal

sow rm example -r demo -d el9 --check

The preview reports membership removals and predicted file changes without changing repository state. Check prediction_complete in JSON before treating the file list as complete. --check cannot be combined with --skip, --timeout, or --no-wait.

See Configuration for the full schema and sow add for input results.

3.5 - Signing Model

Configure metadata and RPM package signing, reference keys, and rotate signing identities.

Configure metadata signing to let clients verify repository indexes. Configure RPM package signing separately when clients also need to verify signatures embedded in RPM files.

What gets signed

Setting Signed content Output Client setting
signing.rpm.metadata RPM index manifest repodata/repomd.xml.asc DNF repo_gpgcheck=1
signing.deb.metadata APT release manifest InRelease, Release.gpg APT Signed-By
signing.rpm.packages RPM package Embedded OpenPGP signature DNF gpgcheck=1

Metadata signing leaves package bytes unchanged. RPM package signing changes package bytes and therefore their SHA-256. SOW does not add signatures to DEB package files.

Configure metadata signing

repos:
  demo:
    signing:
      rpm:
        metadata:
          key: file:///secure/repo-signing.asc
      deb:
        metadata:
          key: file:///secure/repo-signing.asc
    dists:
      el9:
        format: rpm
      trixie:
        format: deb

RPM and DEB can use the same key or different keys. For an encrypted file or environment key, add a passphrase reference. Validate the settings, then build:

sow config check
sow build -r demo
sow check -r demo

Signed empty Dists receive the same signature files as populated Dists. The signed body of InRelease matches Release. Without a configured key, SOW produces unsigned indexes. Managed signing settings come from sow.yml; build has no per-run signing override.

Key and passphrase references

Metadata key reference Meaning Signing implementation
keys/repo.asc OpenPGP key file relative to the workspace root In-process
file:///secure/repo.asc OpenPGP key file at an absolute path In-process
env://SOW_METADATA_KEY Armored key material, file path, or GPG key identifier Depends on the value
agent://KEY_ID_OR_FINGERPRINT Key available through the local GPG environment External gpg

Key files support ASCII-armored and binary OpenPGP data. A GPG key identifier is a 16-digit key ID or a 40- or 64-digit fingerprint. References that resolve to a GPG key identifier use external gpg for private-key access and prompting.

A signing key reference must resolve to exactly one primary key. A trusted_keys file can contain several complete public keys, including consecutive armored blocks. SOW reads the complete bundle and rejects malformed or truncated input. It does not silently use only the first key.

For private-key material, passphrases accept path, file://, or env:// references. GPG key identifiers use GPG passphrase handling and cannot take a passphrase field. See Key references for path and size limits.

config show displays references and resolved public identities, not private-key or passphrase contents. config check resolves references and checks their suitability. For new metadata signatures, the selected signing key must be usable at the publication time and at the current time. Historical verification uses the retained certificate.

New APT signatures use SHA-256. A new publication requires SHA-256, SHA-384, or SHA-512 in both APT signature files. If upgrading a workspace with SHA-1 metadata signatures, run build before starting a new publication. An existing publication attempt resumes its recorded files.

Configure RPM package signing

signing:
  rpm:
    packages:
      mode: fill
      key: agent://7F721C4AD40F4A9D8CA578BFAC7E4690B50CCF3B
      trusted_keys: [keys/upstream.asc]
Mode Behavior
never Store the input bytes unchanged
fill Keep a package whose signature verifies with the configured key or trusted keys; sign other packages
always Keep a package already signed by the configured key; re-sign other packages

With no key, the default mode is never. With a key, the default is fill. Both fill and always require a key.

Package signing uses the host rpm --addsign or rpm --resign command on a private copy. Install rpm and gpg, and make the matching private key available in the GPG environment used by rpm. A key reference identifies the signer; it does not import a private key into that environment. The original input file is unchanged.

SOW checks the result’s signature, package coordinate, and signature-neutral payload digest. If an existing object has the same coordinate and payload and satisfies the current policy, SOW can reuse its signed bytes. In never mode, reuse requires an exact byte match.

Use RPM format v4 for package-signing workflows. SOW does not support the signature tag used by RPM format v6. This is separate from OpenPGP packet versions; historical CentOS OpenPGP v3 and v4 signatures are supported.

Change keys

A changed signing identity makes affected Dists dirty. For metadata keys, run build to create signatures with the new identity. Distribute the new public key to clients before publishing the new metadata.

Existing pool objects are immutable. Changing RPM package policy does not re-sign them in place. For a staged package-key rotation:

  1. Set the new key as current and use mode: fill.
  2. Keep the old public key in trusted_keys while old packages remain members.
  3. Add new package releases signed by the new key.
  4. Remove old trust after the old signed packages have been withdrawn or replaced.

Switching directly to always with a new key requires every Desired RPM to satisfy that key policy. If changed signatures would replace an existing package URL with different bytes, publish a new package release and filename.

RPM verification evaluates certificate and signing-subkey validity at the package’s signature time. Renewing a certificate does not itself invalidate old signatures when the required historical self-certifications and subkey bindings remain in the certificate. Keep that historical public-key material when exporting renewed keys.

Plain signing

sow create /srv/offline --sign-with 6D5C5A26C36B1F73
sow create /srv/offline --sign-with 6D5C5A26C36B1F73 --overwrite

Plain mode signs RPM package files only. Without --overwrite, it signs packages with no parseable embedded signature. With --overwrite, it re-signs all retained RPMs. The key is 16, 40, or 64 hexadecimal characters, with an optional 0x prefix. At least one RPM must remain after any --pigsty cleanup.

See Sign your repository for client setup and Configuration for all signing fields.

3.6 - Transactions & Recovery

Recover interrupted Managed operations and understand local build order and lock behavior.

Managed mode records changes before it applies them. After an interruption, SOW uses these records to finish or roll back the operation. The recovery command depends on the operation that stopped.

Recover an interrupted operation

Start with the recorded state:

sow status -r demo
sow log -r demo
State Next step
clean Run sow check before delivery
dirty Run sow build to apply pending membership or configuration changes
recovering Use the recovery command for the interrupted operation, listed below
error Inspect the reported file or operation in the diagnostic and log
Interrupted operation Recovery command
add, rm, Dist change, or build sow build -r demo
init, repo new, or repo rm Rerun the corresponding lifecycle command
Publication sow publish TARGET
Plain index generation Rerun sow create with the original options

First correct any reported input, configuration, or storage error. Keep the workspace configuration, database, and private state together during recovery. After a local recovery, run sow check -r demo. See Troubleshooting for common errors and publish for publication recovery.

What SOW records

Record Location Purpose
Workspace lifecycle journal .sow/workspace-ops/active.json Coordinates configuration with repository creation or removal
Repository operation journal .sow/<repo>.db Records local membership changes, builds, and maintenance
Publication state .sow/<repo>.db Records target bindings, frozen publication plans, and progress

Workspace lifecycle recovery compares the current sow.yml digest with the journal’s old and new configurations. The configuration replacement decides whether to roll back or finish that lifecycle change. A digest matching neither is an integrity error.

Repository mutations record their own state transitions:

planned → staged → applied → built → done
                       └────────────→ done_dirty
State Meaning
planned The operation and its intended scope are recorded
staged Inputs have been prepared for the next stage
applied Desired state has committed; the previous public generation can still be active
built The generated public tree has been installed
done The operation is complete
done_dirty Desired state has committed and a build remains pending

Other terminal outcomes include rolled_back and failed. Read transitions and timestamps with sow log OPERATION -r demo.

--skip normally ends at done_dirty. A recognized signing-policy change, configuration change before a build plan is frozen, or supported contract upgrade can also leave a committed operation dirty. The log records the reason. Correct the configuration if needed, then run build again.

Errors after a committed change

Database commit and later maintenance are separate steps. A checkpoint or cleanup error can occur after package membership or a build has committed. SOW keeps the committed result in command output. It does not report those items as rolled back.

Read the result and sow status, then inspect the recorded operation with sow log. Resume the command named in the diagnostic. Keep original input files until you have confirmed the result.

Local build order

SOW stages and validates output on the same filesystem, then applies it in this order:

package bytes → metadata → client pointers → obsolete metadata cleanup
  1. Put package bytes in the canonical pool/ path.
  2. Install checksum-named RPM metadata and APT direct and by-hash indexes.
  3. Install RPM repomd.xml and configured signatures, or APT Release, InRelease, and Release.gpg as applicable.
  4. Remove obsolete metadata after the replacement is durable.

The build keeps the immediately preceding RPM immutable metadata and APT by-hash objects available for readers of the previous pointer. Explicit generation retention is separate; see retain.

A dirty repository still serves its last built generation. A multi-Dist operation can switch views at different times. Remote HTTP requests and caches also have their own lifetimes; use sow publish for live target updates. sow changes lists local file changes and does not perform publication or target cleanup.

Locks and waiting

SOW uses advisory locks to coordinate its processes. Workspace lifecycle uses .sow/workspace.lock; repository writes use .sow/repo-locks/<repo>.lock. When both are required, the workspace lock comes first.

Read commands that need a consistent snapshot use shared locks. status probes the repository writer lock so it can report activity without waiting for that writer. Plain create locks its directory and parent.

Option Behavior
-T 30s Wait up to 30 seconds for a write lock
-T 0 Wait until the lock is available; default
-N Try once; exit immediately if the lock is held

A failed lock attempt exits 4. Combining -N with a nonzero -T exits 2. Use only the lock options accepted by the command; read-only commands do not accept them.

Filesystem requirements

Use a local POSIX filesystem with advisory locks, fsync, and atomic rename. On macOS, use APFS. Keep public files and private staging on the same filesystem. SOW rejects symlinked control paths, unexpected file types, and path escapes. It checks file identity before replacing or removing controlled objects. Network filesystems are not supported workspace locations.

Repository and Dist names use [a-z0-9][a-z0-9._-]*; reserved names are listed in Repository layout. For script responses to interruption, lock contention, or integrity errors, see Exit codes.

3.7 - Observability & Audit

Inspect state, verify repository content, compare generations, and read operation history.

Use status to inspect state, check to verify a repository, changes to list file changes, and log to read operation history.

Command Answers
status Does the repository need a build or recovery? Is a writer active?
check Are state, package bytes, indexes, and signatures consistent and ready for delivery?
changes Which local files changed between built generations?
log Which operations ran, and what did they change?
retain ls Which generations are explicitly retained?

These commands do not modify repository contents.

Inspect state

sow status -r demo

The result includes Desired revision, built generation, dirty Dists, pending package count and bytes, writer-lock state, and ready_to_copy.

State Meaning
clean Desired and built state agree
dirty Membership or configuration needs a build
recovering Recorded work needs recovery or cleanup
error Recorded state or file evidence has an unresolved error

status reports state without hashing the public tree. For dirty, run build. For recovering or error, follow the diagnostic and recovery procedure. Run check before delivery.

Verify the repository

sow check -r demo

A normal check reports nine layers:

Layer Checks
config Configuration and effective Dist settings
state SQLite schema and relational state
public-modes Public file and directory permissions
retained Retained generation records and metadata
package-bytes Pool and pending bytes against recorded SHA-256
desired-membership Package identity, architecture, and membership
index Index contents and referenced files
signature Configured metadata and RPM package signatures
generation-manifest Built file manifest against the public tree

An unfinished layout transition instead reports config, state, public-modes, and layout-transition. Complete or abort that migration as directed by repo migrate.

check reads and hashes each unique physical package regardless of cached fingerprints. It does not repair files. A dirty or recovering repository fails readiness even if its previous built tree remains consistent.

Compare generations

sow changes -r demo
sow changes 0 -r demo
sow changes 42 -r demo --json

No base compares with the previous built generation. Base 0 lists the complete current tree. Base 42 compares recorded generation 42 with the current one. Rows contain the action, phase, repository-relative path, size, and SHA-256 as applicable.

This comparison does not inspect a publication target. Use sow publish TARGET to update that target and resume interrupted transfers. Use target GC to reclaim remote objects.

Retain generations and collect unused packages

retain add accepts the current built generation only. If status reports generation 42, retain it before a later build replaces it:

sow retain add 42 -r demo
sow retain ls -r demo

The command verifies the generation and saves its metadata and references. Package files remain in the shared pool. When generation 42 is no longer needed, remove its retention record with sow retain rm 42 -r demo. Then run sow gc -r demo to collect unused bytes. Local gc requires a clean repository; build pending changes first. It preserves packages needed by Desired, built, and previous-client membership records, retained generations, recovery, or publication state. See gc for the complete rules.

The previous-generation metadata kept during ordinary builds is separate from explicit retention. For target cleanup, run sow gc TARGET. Filesystem targets support conditional deletion after cache grace; R2 targets report candidates without deleting them.

Read and export the log

sow log -r demo
sow log OPERATION -r demo
sow log export operations.jsonl -r demo
sow log prune 2026-01-01 -r demo

The log records operation state, times, package results, membership changes, and file changes where applicable. An operation detail can include build-progress events.

log export writes JSON Lines to a new file or stdout. log prune accepts a date or RFC 3339 timestamp and removes eligible terminal audit records. Required recovery records and generation manifests remain available.

See JSON output for scripts and Transactions and recovery for interrupted operations.

4 - Reference

Look up configuration, package references, file paths, command results, and troubleshooting steps.

Use these pages to look up configuration fields, package references, file paths, and script interfaces. For command syntax, see Commands or run sow help COMMAND.

Fields, defaults, validation rules, signing references, and publication targets in sow.yml.

Select packages by digest, coordinate, filename, or name. Resolve ambiguous matches.

Locate package files, indexes, private state, and publication output.

Interpret command results and choose the next action in scripts.

Read the sow.cli/v1 response and command result fields.

Find the next step for configuration, build, signing, and publication errors.

Example conventions

Command blocks omit the shell prompt so you can copy them. Uppercase words such as NAME and DIR in syntax descriptions are placeholders. Square brackets mean optional arguments; ... means repeatable arguments; | separates alternatives.

Output paths, IDs, hashes, timestamps, and counts depend on the workspace. Keep operation and generation IDs as strings; see JSON output.

4.1 - Troubleshooting

Find the next command for migration, pending builds, interrupted publication, locks, and integrity errors.

Start with the error message and the exit code. For Managed commands, specify the workspace and Repository so you inspect the intended state:

sow version
sow status -C /srv/sow -r pgsql
sow log -C /srv/sow -r pgsql

Replace /srv/sow, pgsql, and the target name prod below with your configuration.

The database needs migration

SOW 0.6.1 uses schema v13, as do 0.5.0 and 0.6.0. An upgrade from either version needs no schema migration. For a 0.3 or 0.4 workspace, stop writers and back up the complete workspace. Then run:

sow repo migrate pgsql -C /srv/sow
sow build -C /srv/sow -r pgsql
sow check -C /srv/sow -r pgsql

Repeat this for each Repository. Compatible read-only commands can still inspect an older schema, but they do not migrate it. See Upgrade.

Status shows pending changes

add --skip, rm --skip, and configuration edits can leave Desired state ahead of Built state. Apply the pending changes, then check the result:

sow build -C /srv/sow -r pgsql
sow check -C /srv/sow -r pgsql

A local build updates local indexes. To update a configured remote target, also run sow publish prod -C /srv/sow. See sow status for the state fields.

A command was interrupted

For Plain mode, resolve the reported input or filesystem issue, then rerun the original sow create command with the same Pigsty, timestamp, and signing options.

For an interrupted add, rm, Dist change, or build, inspect sow log and fix the reported cause. Run sow build for that Repository to complete recovery. Committed Desired changes remain part of the pending build. For interrupted init, repo new, or repo rm operations, rerun the corresponding lifecycle command to recover the workspace journal.

For an interrupted publication, restore access to its storage and public endpoint, then rerun the same target:

sow publish prod -C /srv/sow

SOW resumes the saved attempt. To abandon an attempt before commit intent, use sow publish prod --abort -C /srv/sow. After commit intent, rerun publication to finish it. See Publication recovery.

Errors after a committed change

Database commit and later maintenance are separate steps. A checkpoint or cleanup error can occur after package membership or a build has committed. SOW keeps the committed result in command output. It does not report those items as rolled back.

Read the result and sow status, then inspect the recorded operation with sow log. Resume the command named in the diagnostic. Keep original input files until you have confirmed the result.

Target configuration changed

Compare sow.yml with the target named in the error. If you intentionally changed its name, public_endpoint, or max_cache_ttl, accept the change with:

sow publish prod --rebind -C /srv/sow

A change to the Repository, provider, storage endpoint, region, bucket, or prefix requires a new target. Complete pending target maintenance before changing its TTL. See Target bindings.

A command waits for a lock

Another process is using the workspace or Repository. Let that command finish, or stop it through its normal process controls. Retry your command afterward.

For write commands that support lock options, use --no-wait to return immediately when the lock is busy, or --timeout 30s to bound the wait. These options do not apply to init or read commands. Shared-lock reads wait until the writer finishes or you cancel the command; status normally reports the lock state without waiting. Keep the lock files in place: SOW uses their stable paths to coordinate processes. See Global options.

A package, signature, or path fails validation

Use the named file and the check stage to narrow the cause:

Error Action
Same Pool path, different bytes Compare the input with the existing package. Give different content a new package version and file name. Moving it to another input directory does not change its Pool URL.
File name differs only by case Use the already recorded spelling. Case aliases cannot create another Pool URL.
Missing or altered Pool file Restore the original bytes from a matching backup, then rerun sow check.
Signing key unavailable Check the configured key, secret-key access, GPG agent, and current key validity. Retry the build after correcting them.
RPM does not match the trust policy Check the package signature and configured public keys. See Signing.
Public URL returns old content Check the HTTP root, URL prefix, cache, and max_cache_ttl, then retry publication.

sow check reports integrity errors; it does not rewrite damaged data. Keep the reported path and operation ID when investigating. The layout reference explains which files belong to the package Pool, generated indexes, and private state.

4.2 - sow.yml Reference

Workspace fields, defaults, validation rules, signing references, and publication targets.

sow.yml configures a Managed workspace. It declares repositories, Dists, package policy, signing, and publication targets. Plain sow create does not read it.

After editing the file, validate it and inspect the effective settings:

sow config check
sow config show --all

config check validates the YAML, compares it with initialized repository state, and resolves signing references. --all expands defaults in config show.

File rules

  • Use one YAML document and schema: sow/v3.
  • Unknown fields and duplicate keys are errors.
  • Keep the file at the workspace root as a regular file with one hard link, at most 16 MiB. A symlink is rejected.
  • init, repo new/rm, and dist new/rm update the file as part of their operations.
  • Edit policy, signing, and target settings in the file. Defaults are applied when read; reading does not rewrite the source.

Use repo migrate to upgrade an older repository database or layout.

Workspace

schema: sow/v3
architectures: [x86_64, aarch64]
repos: {}
targets: {}
Field Type Default Meaning
schema string Required Must be sow/v3
architectures string list [x86_64, aarch64] Architecture families allowed in this workspace
repos map Empty Repository names and settings
targets map Empty Publication target names and settings

Architecture aliases normalize as follows:

Input Canonical family
x86_64, amd64 x86_64
aarch64, arm64 aarch64

A specified architecture list must not be empty or contain duplicate families after normalization. noarch and all are package architectures, not configuration families. SOW includes those neutral packages in each applicable view.

Repository

repos:
  demo:
    protected: false
    signing: {}
    dists:
      el9:
        format: rpm
Field Type Default Meaning
protected boolean false Blocks repo rm, including -f
signing map Unsigned metadata; package mode never Repository signing settings
dists map Empty Dist names and settings

protected does not block package changes or Dist removal. To remove a protected repository, set it to false, validate the configuration, then run repo rm.

Repository, Dist, and target names must match [a-z0-9][a-z0-9._-]*. Reserved names are ., .., .sow, pool, dists, sow.yml, workspace.lock, workspace-ops, and repo-locks. Repository names must also have distinct private state paths; for example, db and db.db cannot coexist.

Dist

dists:
  el9:
    format: rpm
    architectures: [x86_64]
    limit: 2
    exclude:
      - kind: [debuginfo, debugsource, llvmjit]
Field Type Default Meaning
format string Required rpm or deb; cannot change after initialization
architectures string list Workspace list A nonempty subset of workspace families
limit integer 0 Newest versions to keep per binary name and canonical architecture; 0 keeps all
exclude rule list Empty Package exclusion rules

Adding an architecture makes the Dist dirty; run build to generate its view. Removing an architecture still referenced by membership or built state is rejected.

limit must be non-negative. RPM uses epoch-version-release ordering; DEB uses Debian version ordering. Neutral packages form a separate group. Relaxing policy does not restore removed members; add their package files again.

Exclusion rules

exclude:
  - kind: [debuginfo, debugsource, dbgsym, dbg, llvmjit]
  - name: ["test-*", "*-experimental"]
    arch: [aarch64]

Fields within one rule use AND. Patterns within one field use OR. Rules use OR. exclude runs before limit over the complete candidate membership of each selected Dist.

Field Matches
name Binary package name
source Normalized RPM SOURCERPM or DEB Source name
arch x86_64, aarch64, or neutral
kind RPM: debuginfo, debugsource, llvmjit; DEB: dbgsym, dbg; otherwise main
format rpm or deb

Patterns are case-sensitive strings or shell globs (*, ?, [...]). Empty rules, empty or untrimmed patterns, duplicates within one field, and invalid globs are errors. Pattern values are not checked against the possible package values; a typo can match nothing. kind comes from the corresponding package-name suffix.

See Membership policy for examples and result interpretation.

Signing

Signing applies to a repository. RPM packages, RPM metadata, and DEB metadata have separate settings:

signing:
  rpm:
    packages:
      mode: fill
      key: agent://7F721C4AD40F4A9D8CA578BFAC7E4690B50CCF3B
      trusted_keys: [keys/upstream.asc]
    metadata:
      key: keys/repo-signing.asc
      passphrase: env://SOW_METADATA_PASSPHRASE
  deb:
    metadata:
      key: keys/repo-signing.asc
      passphrase: env://SOW_METADATA_PASSPHRASE

RPM package settings

rpm.packages field Default Meaning
mode never without a key; fill with a key never, fill, or always
key None Signing-key reference; required for fill and always
trusted_keys Empty Additional public-key references accepted by fill

never preserves input bytes. fill preserves packages whose signatures verify with the current or trusted keys and signs the rest. always requires the current key, re-signing packages when needed. The public half of key is trusted automatically. Duplicate trusted-key reference strings are rejected.

Package signing calls the host rpm command on a staged copy. Its GPG environment must already contain the private key. A configured key file alone does not provision it.

Metadata settings

Both rpm.metadata and deb.metadata accept:

Field Default Meaning
key None Metadata signing-key reference
passphrase None Reference to the passphrase for an encrypted key

RPM signing creates repodata/repomd.xml.asc. DEB signing creates InRelease and Release.gpg. Without a key, indexes are unsigned. DEB package files are not re-signed.

Key references

Form Example Value
Path keys/repo.asc OpenPGP key file; relative paths start at the workspace root
file:// file:///secure/repo.asc OpenPGP key file
env:// env://SOW_METADATA_KEY Variable containing armored key material, a file path, or a GPG key identifier
agent:// agent://7F721C4AD40F4A9D8CA578BFAC7E4690B50CCF3B Key in the local GPG environment

Environment names use [A-Za-z_][A-Za-z0-9_]*. A GPG key identifier is a 16-digit key ID or a 40- or 64-digit fingerprint. Hexadecimal letters are case-insensitive.

Key files accept ASCII-armored or binary OpenPGP data and must be regular files without symlinks, at most 16 MiB. Relative paths resolve from the workspace root. Metadata signing uses the supplied private-key material inside SOW. A GPG key identifier uses external gpg, whether supplied through agent:// or an environment variable.

A signing key reference must resolve to exactly one primary key. A trusted_keys file can contain several complete public keys, including consecutive armored blocks. SOW reads the complete bundle and rejects malformed or truncated input. It does not silently use only the first key.

Passphrase references

Passphrases accept path, file://, or env:// references. They require a key containing private-key material; GPG handles the passphrase when a reference resolves to a key identifier. Passphrase values must be nonempty, contain no NUL bytes, and fit within 64 KiB. SOW removes one trailing newline, including CRLF. Passphrase files use the same path rules as key files.

Reference syntax errors return 2; unresolved or unusable signing references return 6.

config show can include key_fingerprint even without --all. Active RPM package settings can also include key_snapshot_sha256, trusted_key_fingerprints, and trusted_key_snapshot_sha256s. Fingerprint and snapshot arrays are independently sorted; do not pair them by index. These fields describe public identities and are output fields, not accepted sow.yml keys.

See Signing for rotation and package reuse.

Publication targets

targets:
  mirror:
    repository: demo
    provider: filesystem
    endpoint: file:///srv/mirror
    prefix: demo
    public_endpoint: file:///srv/mirror/demo/
    max_cache_ttl: 0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true
Field Required Meaning
repository Yes Existing repository name
provider Yes filesystem or r2
endpoint Yes Canonical file:///absolute/path without a trailing slash, or https://host for R2
region R2 only Must be auto; forbidden for filesystem
bucket R2 only Lowercase DNS-safe bucket label; forbidden for filesystem
prefix No Relative storage prefix; empty uses the storage namespace root
credential R2 only env://NAME or file:///absolute/path; forbidden for filesystem
public_endpoint Yes Canonical verification URL ending in /; filesystem accepts HTTP(S) or file://, R2 accepts HTTP(S)
max_cache_ttl Yes Canonical non-negative Go duration, including explicit 0s
authoritative_workspace Yes Must be true
single_writer Yes Must be true
exclusive_write_authority Yes Must be true

The last three fields state that this workspace owns publication and is the only writer for the target namespace. Targets on the same storage cannot have overlapping prefixes. Filesystem targets cannot resolve to overlapping directories.

Use canonical durations such as 0s, 30m0s, or 24h0m0s; 24h is rejected. Durations are bounded below Go’s maximum duration to leave room for the cache-grace margin. Set this value to cover the longest cache lifetime used by the public service.

Create the filesystem endpoint directory before publishing. It must be a real canonical directory, not a symlink. SOW creates the prefix beneath it.

An R2 target adds storage and credential fields:

targets:
  prod:
    repository: demo
    provider: r2
    endpoint: https://0123456789abcdef.r2.cloudflarestorage.com
    region: auto
    bucket: packages
    prefix: demo
    credential: env://SOW_R2_CREDENTIAL
    public_endpoint: https://repo.example.com/demo/
    max_cache_ttl: 24h0m0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true

The referenced environment variable or file contains one JSON document:

{"access_key_id":"YOUR_ACCESS_KEY_ID","secret_access_key":"YOUR_SECRET_ACCESS_KEY"}

It can also contain session_token. Unknown fields, trailing data, missing credentials, and values larger than 64 KiB are rejected. Credential files must resolve to regular files; a symlink to a regular file is accepted. Credentials are not included in the public tree or configuration output.

The first publication binds the repository, provider, storage identity, and prefix. To change the target name, public_endpoint, or max_cache_ttl, use publish --rebind. Provider, endpoint, region, bucket, prefix, and repository identity cannot be rebound; configure a new target. Pending maintenance can block changes to cache TTL or the public endpoint until that maintenance completes.

Fixed settings

Repository paths are always <workspace>/<name>. APT components are always main. Architecture views are derived from Dist configuration and package headers. Generation retention is managed with retain, not a configuration count.

See Layout, Publication, and Exit codes.

4.3 - Package References

Select packages by digest, coordinate, filename, or name, and resolve ambiguous matches.

rm, show, and where accept package references. add accepts file or directory paths. Use sow ls to find a package coordinate or SHA-256, then pass it to another command.

sow ls -r demo -d el9
sow show 'rpm:example-0:1.0-1.x86_64' -r demo

Accepted forms

Form Syntax Matches
SHA-256 sha256:<64 lowercase hex digits> Stored package bytes
RPM coordinate rpm:<name>-<epoch>:<version>-<release>.<arch> Complete RPM identity, including epoch
DEB coordinate deb:<name>=<version>:<arch> Complete DEB identity
Filename example-1.0-1.x86_64.rpm Stored package filename
Bare name example Binary package name

SHA-256 values must be complete and lowercase. Abbreviations and uppercase digests are rejected with exit 6. The digest is for the stored bytes; Managed RPM signing can make it different from the input file’s digest.

RPM coordinates always include an epoch, using 0 when the package has none. A reference with the rpm: prefix is matched against the complete stored coordinate. Include every coordinate component to match the package.

DEB coordinates include the complete Debian version, with any epoch and revision, plus the native architecture. Use amd64, arm64, or all for DEB and x86_64, aarch64, or noarch for RPM. These values come from the package, not the configuration aliases.

sow where 'rpm:example-0:1.0-1.noarch'
sow where 'deb:example=2:1.0-1:amd64'

A filename is a convenience, not a unique identifier. Use a coordinate or full digest in scripts. References do not support paths, globs, regular expressions, or version ranges.

Name matching and ambiguity

Command Bare name matching several versions or architectures
rm Removes all matching memberships in the selected Dists
show Rejects the ambiguous reference and lists candidates
where Requires one package identity; returns all repository locations for that identity

Preview a name-wide removal before applying it:

sow rm example -r demo -d el9 --check

For show or where, select a coordinate or digest from the candidate list. where can return several repositories when they contain the same format, coordinate, and SHA-256. If repositories contain different versions or bytes for the reference, use -r or a more specific reference.

Other reference forms must identify one object within a repository. A missing or ambiguous reference returns exit 6.

Search scope

rm and where search Desired membership. Without -d, show searches all recorded package objects, including packages that no longer belong to a Dist. With -d, it searches Desired membership in those Dists. Unrecorded files are not query results.

Command Default scope Selection
rm Selected Dists of one repository -r; repeatable -d, or inferred Dist
show All recorded package objects in one repository -r; optional -d limits to Desired membership
where All repositories in the workspace Optional -r and -d

For rm, if the Dist cannot be inferred and the repository has several Dists, supply -d. Without -r, where -d NAME searches repositories containing that Dist name. It reports an error if the requested Dist name exists nowhere.

Coordinate conflicts

Within a repository, one coordinate identifies at most one content object. SOW rejects a different package with an existing coordinate and does not replace it silently. Changing only a signature still changes the exact-byte identity.

With active RPM signing, SOW can reuse an existing signed object when its coordinate, signature-neutral payload, and signing policy match. Otherwise, use a new release and filename for changed bytes. See Signing.

See rm, show, and where for command options.

4.4 - Repository Layout

Locate package files, repository indexes, private state, and publication output.

Serve the complete Managed repository directory: pool/ plus dists/. Keep workspace configuration and .sow/ state outside the web server’s document root.

Plain directory

/srv/offline/
├── example-1.0-1.x86_64.rpm
├── example_1.0-1_amd64.deb
├── repodata/
│   ├── <sha256>-primary.xml.gz
│   ├── <sha256>-filelists.xml.gz
│   ├── <sha256>-other.xml.gz
│   └── repomd.xml
├── Packages
├── Packages.gz
└── repo_complete                     # with --pigsty

sow create writes RPM metadata only when RPMs are present and DEB metadata only when DEBs are present. RPM locations use a bare basename; DEB locations use ./<basename>. A running build uses private .sow-plain-stage-* directories. Exclude them from serving and copying. A later create removes stale SOW temporary paths and rebuilds the indexes.

.sow-plain-rpm-history.json records the current and previous RPM metadata references. Keep it when copying a directory for further create runs. It does not belong in a client index and is not a recovery journal.

Managed workspace

<workspace>/
├── sow.yml                           # workspace configuration
├── .sow/                             # private state
│   ├── workspace.lock
│   ├── workspace-ops/
│   ├── repo-locks/<repo>.lock
│   ├── <repo>.db
│   └── <repo>/
│       ├── abandoned/                # retained migration stages
│       ├── stage/
│       ├── recovery/
│       ├── retained/
│       ├── transitions/
│       └── pending/
└── <repo>/                           # public repository root
    ├── pool/
    └── dists/

The database can have SQLite WAL and shared-memory sidecar files. .sow/ and pending directories are private (0700). Pending payload files use 0644 so they can be promoted to public paths without changing their mode.

SOW 0.6.1 uses database schema v13 and configuration schema sow/v3. These are separate version identifiers. Back up the complete workspace before repo migrate. Keep the database with its matching private and public state; do not edit its schema version or rebuildable cache by hand.

Package pool

pool/<prefix>/<source>/<filename>

source comes from the RPM SOURCERPM or DEB Source field, normalized to a source package name. If absent, SOW uses the binary package name. The prefix is the first character converted to lowercase. If the source starts with lowercase lib, use its first four characters, or the whole name when it is shorter, converted to lowercase.

Source Example path
postgresql-18 pool/p/postgresql-18/libpq5_18.3-1_amd64.deb
example pool/e/example/example-1.0-1.x86_64.rpm
libfoo pool/libf/libfoo/libfoo1_1.0-1_amd64.deb

Package objects are immutable. Membership removal leaves the bytes available to built, retained, recovery, and publication records. gc removes them when those records no longer need them. Each repository owns its own pool.

RPM Dist

dists/el9/
├── x86_64/repodata/
│   ├── <sha256>-primary.xml.gz
│   ├── <sha256>-filelists.xml.gz
│   ├── <sha256>-other.xml.gz
│   ├── repomd.xml
│   └── repomd.xml.asc                # with metadata signing
└── aarch64/repodata/
    └── ...

Architecture views contain metadata only. Each package location points back to the root pool:

<location href="../../../pool/e/example/example-1.0-1.x86_64.rpm"/>

Native packages appear in their matching architecture view. noarch packages appear in all configured views. Previous checksum-named metadata can remain alongside the current files; repomd.xml selects the active set.

For a self-contained directory with local package paths, use export rpm-leaf.

DEB Dist

dists/trixie/
├── Release
├── InRelease                         # with metadata signing
├── Release.gpg                       # with metadata signing
└── main/
    ├── binary-amd64/
    │   ├── Packages
    │   ├── Packages.gz
    │   └── by-hash/SHA256/<digest>
    └── binary-arm64/
        └── ...

Packages entries use root-relative archive paths such as Filename: pool/e/example/example_1.0-1_amd64.deb. Release contains SHA-256 manifests and Acquire-By-Hash: yes. Architecture all packages appear in each applicable index.

Publication paths

Both target providers receive the same public layout:

<prefix>/
├── pool/
└── dists/

An export has its own package files, metadata, manifest, and .sow-export.json completion marker. It is a separate delivery artifact and is not another Managed repository.

Names and case sensitivity

Repository, Dist, and target names use [a-z0-9][a-z0-9._-]*. Reserved names include ., .., .sow, pool, dists, sow.yml, workspace.lock, workspace-ops, and repo-locks.

A new pool path that differs only in case from a known full path is rejected, even for identical bytes. On a case-insensitive filesystem, source directories that differ only in case also conflict. On a case-sensitive filesystem those source directories can remain distinct, so check them before moving a repository to a case-insensitive volume.

See Views, Serving, and Platforms.

4.5 - Exit Codes

Exit codes 0–6 and interruption code 130, with meanings and examples.

Use exit codes in scripts and read the diagnostic for the specific cause. SOW uses the same code meanings across commands.

Code Meaning Next step
0 Success or no-op Read the result; no-op commands may report noop: true
1 Runtime I/O, package parsing, rendering, or other unclassified error Correct the reported input or environment problem, then retry
2 Invalid command, workspace discovery, or configuration Check sow help COMMAND, selection flags, and sow config check
3 Partial success Inspect the result to identify committed work and failed items
4 Write lock unavailable Retry later or use a longer --timeout
5 Integrity/recovery error, or repository not ready for delivery Inspect status and check; build a dirty repository or recover the recorded operation
6 Rejected operation Correct the conflict, package reference, architecture, signing setting, or protection setting named in the error
130 Command interrupted by SIGINT or SIGTERM Follow the recovery procedure for the interrupted operation

Success does not always mean ready

status returns 0 when it can report state, including dirty, recovering, or error. Its ready_to_copy field is a state summary, not a full byte check. Use check before delivery:

sow build -r demo
sow check -r demo

A dirty repository can have consistent old indexes and still fail check with code 5, because Desired state has not been built. Run build and check again. If the diagnostic reports corrupted bytes or conflicting recovery evidence, inspect that failure before retrying. See Troubleshooting.

Partial success

For add, code 3 means at least one input was accepted or reused and at least one failed. The JSON result retains per-input outcomes. Exclusion by policy is not a failed input. init can also return 3 after creating some declared objects and failing on a later one.

Capture both output and status before deciding whether to continue:

#!/usr/bin/env bash
set -uo pipefail

sow add ./incoming/ -r demo -d el9 --json > add-result.json
rc=$?
case "$rc" in
  0) ;;
  3) jq '.result.items[] | select(.status == "failed")' add-result.json
     exit 3 ;;
  4) echo 'Repository is busy; retry later.' >&2
     exit 4 ;;
  *) exit "$rc" ;;
esac

sow check -r demo || exit $?
sow publish mirror

mirror must be a configured target for demo. The script stops on partial success so you can inspect failed inputs before publishing.

Lock waits and interruption

-T 0 waits for a write lock without a time limit and is the default. -T 30s waits up to 30 seconds. -N tries once. Lock contention returns 4; -N combined with a nonzero -T returns 2.

Read-only commands do not accept these flags. Some take shared locks for a consistent snapshot; status reports repository writer-lock activity without waiting for that writer.

Ctrl-C or SIGTERM returns 130 when SOW handles the interruption. An internal timeout, such as an upload progress timeout, keeps its normal error class. Interruption does not undo work that already committed. Inspect the state and follow Transactions and recovery to resume the operation.

JSON errors

With --json, stdout contains the result envelope and stderr contains diagnostics. The errors array uses these classes:

Code JSON class
1 runtime
2 usage, discovery, or config
3 partial
4 lock
5 integrity
6 rejected
130 interrupted

Branch on the code or class, not message text. Plain create prints its domain error to stderr; its JSON message can include an extra CLI category prefix. Diagnostic and partial results remain available when ok is false.

See JSON output and Transactions and recovery.

4.6 - JSON Output

Look up JSON response fields, package and operation IDs, errors, and command results.

Use --json on data commands to read one newline-terminated JSON document from stdout. Diagnostics go to stderr. log export is the exception: it writes JSON Lines and does not accept --json.

sow status -r demo --json | jq '.result.status'

Envelope

Every JSON response has these seven fields:

Field Type Meaning
schema string sow.cli/v1
command string Command name, including subcommand, such as repo ls
ok boolean Command succeeded; false when errors are present
repository string or null Selected repository, when attached to the response
operation string or null Command-specific operation or publication identity
result object or null Command data, including useful partial or diagnostic results on failure
errors array Error objects with code, class, and message

Example of a successful configuration check:

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

The actual output occupies one line. Paths and counts depend on the workspace. A failure before meaningful work usually has result: null. A failed check and a partially successful add can still return useful results. Preserve them when ok is false.

A maintenance error after a database commit can also return committed results. Inspect the result, status, and the operation log before retrying. A nonzero exit does not mean that the transaction rolled back.

Errors

{"code":3,"class":"partial","message":"managed: batch partially succeeded"}

Branch on code or class, not message text. Classes are runtime, usage, discovery, config, partial, lock, integrity, rejected, and interrupted. See Exit codes for the mapping and next steps.

Identities

Keep operation and generation IDs as strings. Operation IDs can exceed JavaScript’s exact integer range. Generation IDs are 20-digit, zero-padded decimal strings:

{"operation":"8632724976452398569","built_generation":"00000000000000000004"}

This applies to generation, built_generation, base_generation, and generation-valued base fields. Desired revisions and counts remain JSON numbers.

The top-level operation field is set as follows:

Command Value
add, rm without preview, build, local gc Journal ID, if allocated
publish, publish --abort Publication attempt ID, if available
log ID Requested operation ID
Other commands null; log prune puts its ID in result.operation

A null operation does not mean the command is read-only. where keeps the top-level repository null and places repository names inside its locations.

Plain create

Result field Meaning
dir Absolute indexed directory
rpm, deb Indexed package counts
kept Sorted included filenames
removed Files removed by --pigsty
marker Whether the completion marker was produced
marker_sha256 Marker digest, present with the marker
noop No public changes were needed
recovered Always false; Plain mode has no journal recovery
signed, signer Signed filenames and signer identity, when available

Initialization and configuration

Command Result
init workspace, config_created, repositories_initialized, dists_initialized, existing
config check workspace, repositories, dists
config show Effective configuration: schema, architectures, repos, and targets when present

Resolved signing settings can contain public fingerprints and certificate snapshot hashes. Fingerprint and hash arrays are independently sorted and must not be paired by index. See Configuration.

Repositories and Dists

repo ls returns result.repositories[]. repo new and repo show return one repository object directly. Common fields are name, path, protected, dists, generation, desired_revision, status, packages, memberships, and config. dirty_reasons and recent_operation appear when available. An operation summary has id, kind, state, created_at, and updated_at, plus optional error_class. The same summary shape appears in status.pending_operation and status.recent_operation.

packages counts stored package objects; memberships counts Desired Dist memberships. One object in two Dists counts as one package and two memberships.

dist ls returns result.dists[]. dist new and dist show return one Dist object: name, format, architectures, desired_members, built_members, generation, dirty, status, effective_config_sha256, and config, with optional dirty_reasons.

Each architecture entry has family and ecosystem_arch. DEB uses canonical family x86_64 with ecosystem name amd64, for example.

repo rm and dist rm return name, removed, and noop. repo migrate returns repository, repository_id, from_layout, to_layout, schema_from, schema_to, phase, generation, grace_not_before, legacy_aliases, and complete, plus optional warnings. grace_not_before is null when no grace deadline applies. Migration-abort warnings identify retained temporary stages or incomplete cleanup. Once repository selection succeeds, a migration failure keeps that repository in the response. See repo migrate for phase meanings.

add, rm, and build

All three report repository state after the operation: repository, desired_revision, built_generation, and dirty.

add also returns operation, accepted, failed, memberships_added, memberships_removed, and items[]. Each item has input and status. It also has format, coordinate, sha256, a per-Dist dists map, and error when those values are available.

Item status Meaning
accepted New package accepted into membership
reused Existing package object reused; membership can still change
excluded No selected Dist accepted the package because of policy
failed Input failed; inspect error

Per-Dist outcomes are accepted, excluded, or limited. A package can have different outcomes in different Dists. Membership removal counts can increase when limit removes older versions.

rm returns optional operation, removed[], selected dists, changes[], and check. Each removed record has dist, sha256, coordinate, and name. A preview has check: true; only prediction_complete: true confirms a complete predicted file list. Pool bytes are not removed by rm.

build returns operation, considered dists, and noop. A no-op creates no new generation.

status and check

status returns status, ready_to_copy, desired_revision, built_generation, dirty_dists, dirty_reasons, pending, and repository_locked. Optional fields include pending_operation, recent_operation, and lock_holder.

status is clean, dirty, recovering, or error. pending contains count and bytes. A successful status query can return any repository state. ok: true means the query succeeded. Before publication or copying, run check for a complete validation.

check returns repository, status, ready_to_copy, built_generation, desired_revision, and layers[]. Each layer has name, ok, checked, and issues.

Normal layers are config, state, public-modes, retained, package-bytes, desired-membership, index, signature, and generation-manifest. An active layout transition instead reports the first three and layout-transition. A dirty repository can pass individual consistency layers and still fail readiness with exit 5.

changes

The result has repository, base, generation, dirty, and changes[]. Each change contains:

Field Values
op add, update, delete
phase payload, metadata, pointer, delete
path Slash-separated path relative to the repository root
size, sha256 Present for added/updated files; omitted for deletions

No base compares with the previous built generation. Base 0 lists the complete current tree. A numbered base compares that recorded generation with the current one. This is a local file difference; use publish and target gc for remote changes.

Package queries

Command Result fields
ls repository, selected dists, dirty, packages[]
show repository, package
where reference, locations[]

Package objects include sha256, format, coordinate, architecture, canonical_arch, pool_path, filename, size, name, source, version, kind, storage, created_revision, dists, and built_dists. Optional fields include epoch, release, RPM payload_sha256 and signature_key, and warning.

architecture is native (noarch, amd64, etc.); canonical_arch is x86_64, aarch64, or neutral. storage is pool or pending; it does not describe remote publication. dists records Desired membership; built_dists records the last built membership. Package-object coordinate omits the format prefix; prepend format + ":" to form a CLI coordinate reference. where location coordinates already include the prefix. Each location has repository, dists, built_dists, sha256, and coordinate.

Publication and maintenance

Command Main result fields
publish repository, target, provider, generation, optional attempt/checkpoint, phase, objects, noop
publish --abort repository, target, provider, attempt, phase, objects
retain add/rm repository, record, record_identity, path
retain ls repository, generations[]
Local gc Optional operation, repository, base_generation, generation, objects, bytes, noop
Target gc repository, target, provider, phase, reports, candidates, deleted_objects, deleted_bytes, retained_objects, pending_grace, completed_attempts, noop
export rpm-leaf repository, repository_id, generation, dist, arch, directory, method, signed, signer_identity, packages, files, manifest_sha256

publish --rebind has the normal publish shape. R2 target GC reports candidates without deleting remote objects.

Operation logs

log returns repository and operations[], newest first. log ID returns repository and detail, with operation, duration_ms, events, packages, memberships, and files.

Detail array Record fields
events sequence, state, detail_json, occurred_at
packages sequence, input_path, disposition; optional package_sha256, coordinate, error_class, message, dists
memberships sequence, dist, package_sha256, action
files sequence, action, phase, path; optional size, sha256

payload_json, result_json, and event detail_json are strings containing JSON. New metadata-building operations include publication_time in payload_json. Older journals and layout migrations can omit it. Parse these JSON strings separately when needed:

sow log -r demo --json \
  | jq '.result.operations[] | .payload_json | fromjson'

log prune returns operation, repository, before, pruned, and compaction_deferred. A date-only argument is resolved in the machine’s local timezone; before contains the resolved timestamp.

log export writes one complete operation record per line, without the envelope:

sow log export operations.jsonl -r demo
sow log export - -r demo

A file target must be new and its immediate parent must be a real directory, not a symlink.

Use a validation result in a script

#!/usr/bin/env bash
set -euo pipefail
sow check -r demo --json | jq -e '.ok' >/dev/null
sow changes 0 -r demo --json \
  | jq -r '.result.changes[] | [.phase, .op, .path] | @tsv'

This checks the local repository and prints its current file manifest. To publish it, run sow publish TARGET for the configured target.

4.7 - Platforms & Integrations

Supported platforms, filesystem requirements, repository formats, clients, and integration coverage.

SOW builds RPM and DEB repository files on Linux and macOS. Choose a supported local filesystem for the workspace, then configure HTTP or storage access for clients.

Platforms

Operating system amd64 arm64 Release formats
Linux Yes Yes Archive, RPM, DEB
macOS Yes Yes Archive
Windows No No —

Release binaries use CGO_ENABLED=0 and need no language runtime. Source builds require Go 1.27.3 or later. Archives include the README, changelog, Apache-2.0 license, and third-party notices; Linux packages include license and notice files with the binary. Run sow version to inspect the binary version, platform, and build toolchain.

Workspace filesystem

Use a local POSIX filesystem with advisory locks, fsync, and atomic same-filesystem rename. Network filesystems such as NFS are not supported workspace locations. On macOS, use APFS; SOW rejects builds when required atomic directory exchange is unavailable.

Keep the workspace’s public tree and private staging on the same filesystem. Serve only <workspace>/<repo>/. That complete pool/ + dists/ tree can be copied to other storage without the private database or journal.

Full pool paths that differ only in case are rejected. Source-directory case aliases are also rejected on case-insensitive filesystems. A repository created on a case-sensitive volume can contain source directories that a case-insensitive destination cannot represent; filesystem publication checks this before starting. See Layout.

Repository formats and clients

Output Client entry point Package locations
Plain RPM repodata/repomd.xml Beside repodata/
Plain DEB Packages or Packages.gz Beside the indexes; flat APT source
Managed RPM dists/<dist>/<arch>/repodata/repomd.xml Relative paths back to root pool/
Managed DEB dists/<dist>/Release or signed InRelease Root-relative pool/ paths

Managed APT uses component main and SHA-256 by-hash indexes. Metadata signing produces RPM repomd.xml.asc or APT InRelease and Release.gpg.

Publication times

New Managed operations record one publication time when they build metadata, except sow repo migrate. APT Release dates, RPM repomd.xml data timestamps, and metadata signatures use that time. RPM revision remains the Generation number.

Recovery reuses the recorded time. Older journals without a recorded publication time retain their previous timestamp behavior. No-op operations leave publication times unchanged.

XML and SQLite RPM metadata

SOW generates gzip-compressed primary, filelists, and other XML. It does not generate SQLite primary_db, filelists_db, or other_db. These are distinct metadata formats; a tool that only accepts primary_db needs an XML input path or a separate conversion.

Missing primary_db does not by itself prevent YUM from reading the repository. YUM’s upstream loader falls back to XML and creates its local cache when a suitable SQLite index is unavailable. See the YUM metadata loader. SOW’s generated XML uses gzip, so zstd support is a separate question when comparing other repositories or tools.

SOW does not generate zchunk, modulemd, or source-package indexes. If your clients need module profiles or dnf module install, keep a tool that provides module metadata. For ordinary RPM installation from modular packages, apply the appropriate client module_hotfixes policy; see the migration guide.

dnf reposync

Default dnf reposync confines downloads to one repository directory, so it rejects the parent-relative package locations in a canonical Managed RPM view. This follows its documented safe-write path. Use sow export rpm-leaf to generate a self-contained directory for that workflow.

Existing EL7 caches

EL7 YUM compares metadata data timestamps with its cached repository. Plain create --metadata-timestamp SECONDS lets you preserve and advance an existing publication time. The default is 0.

Exported RPM leaves and RPM metadata generated during sow repo migrate use timestamp 0, with no timestamp override. Later Managed builds use their recorded publication time. For these outputs, use a new repository ID if the existing ID has cached a positive timestamp. Alternatively, clear that ID’s metadata cache. Changing the base URL alone is insufficient. To retain the old ID and cache, follow the timestamp procedure with Plain mode in YUM migration.

RPM signatures

Package-signing workflows support RPM format v4. RPM format v6 signature tags are not supported. Metadata key-reference support is separate; see Signing.

Publication providers

Provider Publication Target GC
filesystem Writes below an existing canonical file:// endpoint Conditional deletion after cache grace, followed by public-absence verification
r2 Writes through S3-compatible storage operations Reports candidates; does not delete remote objects

Both publish the complete repository under the configured prefix. Configure the HTTP service, DNS, credentials, access policy, and cache behavior for that storage. public_endpoint tells SOW where to verify public content; it does not create the service. R2 public endpoints must use HTTP(S). Filesystem targets also accept file:// verification.

Automated coverage

The source tree contains these integration checks:

Check Environment Scope
CLI clean-room workflow Linux CI Shipping binary, mixed Plain output, Managed initialization, RPM/DEB Dists, queries, build, and check
Plain APT client pgsty/u22 Ubuntu 22.04 container HTTP update, discovery, version selection, download, and installation with an explicitly trusted unsigned source
RPM detached-signature transitions AlmaLinux 8/9 and pgsty/el10 containers DNF behavior while repomd.xml and its detached signature change
S3-compatible transport Pinned PGSTY Silo container Bucket listing, HEAD, GET, conditional single-part PUT, replay, and object SHA-256 metadata
Release packaging Linux CI Four archives, two RPMs, two DEBs, checksums, package paths, and license files

The DNF test checks signature transitions; the APT test uses unsigned Plain output. Complete signed Managed installation and hosted R2 conditional multipart completion require checks in the deployment environment.

Before switching clients, run sow check, publish to the intended target, then use the actual client configuration to refresh indexes and install a package. See Serving for client setup.

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

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

Keep the previous RPM indexes

SOW 0.6.1 keeps the checksum-named RPM metadata referenced by the current and immediately previous repomd.xml. A client that fetched the previous index can still retrieve those metadata files after an update. An unchanged run does not advance this retention window.

The hidden .sow-plain-rpm-history.json file records these references. Keep it with the directory when rebuilding a working copy. It is a retention record, not a recovery journal. Plain mode still recovers by rebuilding.

This window does not retain removed package files or every older index. If client caches span several updates, keep the required packages and metadata in your publication layer.

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.

Documentation packages such as annobin-docs are retained unless they match one of those rules.

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

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

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

5.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 to upgrade a 0.3 or 0.4 workspace to SOW 0.6.1. Workspaces already on 0.5.0 or 0.6.0 use schema v13 and need no schema migration. New 0.6.1 repositories also use schema v13.

  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.6.1/
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 plan before its commit decision. It does not undo a completed schema migration. For an earlier C2 transition, SOW checks the repository identity and available journal and database control records. Conflicting records stop the abort. A journal-bound plan also requires its recorded base manifest.

An abort can complete before the first journal or base manifest was written, provided the trusted transition marker already exists. SOW keeps that marker so another sow repo migrate NAME can plan and resume the transition. After commit intent, only forward recovery is allowed.

SOW moves a nonempty temporary stage intact into .sow/<repo>/abandoned/. It reports retained paths or cleanup problems on stderr and in JSON warnings. A stage-cleanup warning does not prevent abandonment. Inspect the reported paths before removing files you no longer need. The public tree remains unchanged.

A fresh development-era C2 repository still cannot start a migration. If an older abort already removed the trusted marker, restore a complete matching backup or import the original packages into a new workspace.

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.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"}
  ]
}

Pending repository maintenance can mark Dists dirty even when membership counts match. Read dirty_reasons and sow status before deciding that a Dist is ready for delivery.

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

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

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

Preview predicts membership removals, policy results, file paths, actions, Generation, and revision. These match execution when package references, selected Dists, repository state, configuration, and signing keys remain unchanged. Execution records its own publication time when it rebuilds metadata. Indexes and signatures that contain time values can therefore differ in bytes, hashes, and sizes.

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

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

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

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

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

Pending repository maintenance also affects Dist status, even when Desired and Built memberships match. Inspect dirty_reasons and the pending operation, then follow the recovery command it identifies. The same condition appears in dist ls and dist show.

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

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

Publication time

Each new build records one UTC publication time, with whole-second precision. APT Release dates, RPM data timestamps, and metadata signatures use this time across the affected Dists. Recovery reuses it; a no-op build leaves it unchanged.

The chosen time cannot precede the recorded time of an existing build or the selected key’s activation time. A key that is not yet usable causes an error. Keep the host clock correct.

Older journals and repo migrate keep their previous timestamp behavior. An rm --check preview chooses its own time, so its predicted hashes and sizes can differ from execution. See Publication times.

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

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

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

5.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. SOW uses up to eight concurrent HEAD requests per page. This limit is internal; publish has no --jobs option. Upload retries do not reset the idle timeout unless data transfer makes progress. 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

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

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

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

In a Repository with several Dists, a later build can update another Dist while this RPM Dist keeps an older Generation. A no-op build does not advance that Dist. Check sow dist show DIST and sow status before exporting. Export after an intended update builds the RPM Dist, or use a separate RPM Repository for this workflow.

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

export rpm-leaf uses a fixed 0 repomd data timestamp and exposes no --metadata-timestamp option. If EL7 clients retain a positive cached timestamp, this output cannot directly replace their existing repository ID. 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

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

New metadata-building operations also record publication_time in payload_json. It is a UTC timestamp with whole-second precision. Recovery reuses it for metadata and signatures. Older journals and layout migrations can omit this field.

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