Replace the metadata generator for a flat RPM repository while preserving publication timestamps, signatures, and client caches.
This is the multi-page printable view of this section. .
SOW Docs
-
1: Get Started
- 1.1: Installation
- 1.2: Quick Start
- 1.3: Your First Workspace
- 1.4: Core Concepts
- 2: Tutorials
-
3: Features
- 3.1: Plain Flat Repositories
- 3.2: Managed Workspaces
- 3.3: Pool & Metadata Views
- 3.4: Membership Policy
- 3.5: Signing Model
- 3.6: Transactions & Recovery
- 3.7: Observability & Audit
-
4: Reference
- 4.1: Troubleshooting
- 4.2: sow.yml Reference
- 4.3: Package References
- 4.4: Repository Layout
- 4.5: Exit Codes
- 4.6: JSON Output
- 4.7: Platforms & Integrations
-
5: Commands
- 5.1: sow create
- 5.2: sow init
- 5.3: sow config
- 5.4: sow repo
- 5.5: sow dist
- 5.6: sow add
- 5.7: sow rm
- 5.8: sow ls
- 5.9: sow show
- 5.10: sow where
- 5.11: sow status
- 5.12: sow build
- 5.13: sow check
- 5.14: sow changes
- 5.15: sow publish
- 5.16: sow retain
- 5.17: sow gc
- 5.18: sow export
- 5.19: sow log
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
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 DIRto 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
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:
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:
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:
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.
Verify the installation
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.
- Stop all workspace writers.
- Back up the complete workspace with a tool that preserves hard links, such as
rsync -aHortar. - Install SOW 0.6.1.
- Build and check each Repository in
sow.yml:
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:
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
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:
Keep packages at the directory top level; sow create does not scan subdirectories.
Generate metadata
With one RPM and one DEB, the output includes:
The directory now has indexes beside the original packages:
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:
If this host has no APT or DNF client, check the HTTP entry point for the format you added:
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:
Refresh the index and install one of your packages:
Save this as /etc/apt/sources.list.d/sow-quickstart.list:
The final ./ selects a flat repository. Refresh the index and install one of your packages:
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:
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
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
init creates:
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
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:
Add packages
Select the target Dist explicitly:
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
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:
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
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
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 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
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.
Create a DEB pool with by-hash indexes and configure an APT client.
Sign metadata and RPM packages, then configure clients to verify signatures.
Publish to a filesystem target and serve it with Nginx.
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
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
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:
Check the configuration:
exclude runs before limit. SOW includes noarch packages in every enabled architecture view. Keep only CPU architecture names in architectures.
3. Add RPMs
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:
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:
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:
Then refresh and query the repository:
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:
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
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
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:
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:
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:
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:
- With the same inputs, options, and timestamp, a repeat build reports
noop=true. Preserve the existing valid detached signature. - 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
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
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:
Then validate it:
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
Replace the input path with your DEB directory. add stores accepted packages once and builds the indexes. The public tree is:
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:
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:
Then refresh and query it:
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
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
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.
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:
Validate the configuration, rebuild, and check:
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:
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:
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:
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:
Then enable the checks that correspond to what you signed:
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:
Reference it from deb822 configuration and do not set Trusted: yes:
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:
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
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
Continue when check succeeds.
2. Add a filesystem target
Create the target’s parent directory and give the SOW operator write access:
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:
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 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:
Check and reload the configuration:
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:
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:
- Run
sow checkon the source. - Copy the complete Repository to a new release directory on the destination. Preserve hard links, for example with
rsync -aHortar. - Verify transferred files against
sow changes 0or an archive checksum manifest. - Switch the server’s path to the new release directory.
- 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
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:
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:
Set limit: 1 for both Dists in ~/repo/sow.yml:
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.
3. Ingest once and build once
Import both package formats, then build their indexes together:
After sow check succeeds, list the formats and architectures read from package metadata:
The output must include at least:
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:
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:
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.
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:
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:
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:
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:
Add the recorded pool objects to the stable Dists:
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:
Pause other repository writers while creating the snapshot. First save the stable package lists:
Add those manifests to the corresponding snapshot Dists:
To keep this build after you eventually remove the snapshot Dists, also retain the current Repository Generation:
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:
3 - Features
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
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.
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:
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
--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
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
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
| 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
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
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:
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.
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:
- Start at
-C/--workdir DIR, if supplied; otherwise start at the current directory. - If no workspace is found, search from
$SOW_DIR. - 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:
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:
- Explicit
-r/--repo NAME. - The repository containing the discovery starting directory.
- The only repository in the workspace.
add, rm, and ls select Dists in this order:
- Explicit
-doptions; repeat the option to select several Dists. - The Dist containing the discovery starting directory.
- 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
A Managed repository stores each package object once under pool/. Its Dists and
architecture views contain indexes that refer to those package bytes.
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:
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:
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:
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.
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
Use Dist policy to exclude unwanted packages and limit the number of versions in indexes.
Set exclude and limit in sow.yml:
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.
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:
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
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 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
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:
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
| 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:
- Set the new key as current and use
mode: fill. - Keep the old public key in
trusted_keyswhile old packages remain members. - Add new package releases signed by the new key.
- 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
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
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:
| 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:
| 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:
- Put package bytes in the canonical
pool/path. - Install checksum-named RPM metadata and APT direct and by-hash indexes.
- Install RPM
repomd.xmland configured signatures, or APTRelease,InRelease, andRelease.gpgas applicable. - 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
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
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
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
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:
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
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
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.
Check supported systems, filesystems, client formats, and integration coverage.
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
Start with the error message and the exit code. For Managed commands, specify the workspace and Repository so you inspect the intended state:
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:
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:
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 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:
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
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:
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, anddist new/rmupdate 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
| 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
| 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
| 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
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:
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
| 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:
The referenced environment variable or file contains one JSON document:
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
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.
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.
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:
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
Serve the complete Managed repository directory: pool/ plus dists/.
Keep workspace configuration and .sow/ state outside the web server’s document root.
Plain directory
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
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
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
Architecture views contain metadata only. Each package location points back to the root pool:
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
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:
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
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:
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:
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
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.
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:
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
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:
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:
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:
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
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
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
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
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:
- Start at
-C/--workdir DIR, when supplied; otherwise start at the current directory. - Search upward and stop at the first
sow.yml. - If that search finds nothing, repeat from
SOW_DIRwhen set. An explicit-Csuppresses the current-directory candidate, but not theSOW_DIRfallback. - If no Workspace is found, exit
2.
--workdir changes only the discovery start directory. It does not change the process working
directory, so relative positional paths still resolve against the actual current directory.
sow create never performs Workspace discovery.
Repository selection
Commands that require one Repository select it in this order:
- explicit
-r/--repo NAME; - the Repository containing the discovery start directory;
- the only Repository in the Workspace;
- otherwise fail with exit
2and list the candidates.
repo new and repo rm take NAME positionally and do not accept -r. sow where searches all
Repositories by default; -r narrows its scope. Publication targets select their configured
Repository, so publish TARGET and gc TARGET do not accept an additional Repository selection.
Dist selection
add, rm, and ls require a concrete Dist set and select it in this order:
- repeated
-d/--dist NAMEvalues; - the Dist containing the discovery start directory;
- the only Dist in the selected Repository;
- otherwise fail with exit
2and list the candidates.
Other commands use these defaults:
build,check, andstatusdefault to all Dists when-dis absent;showsearches the whole selected Repository unless-dnarrows it;wheresearches all matching Dists across the Workspace unless-r/-dnarrow it;changesis Repository-wide and rejects-d.
Locking
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:
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
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
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
.rpmor.debare considered. - No recursion, no symlink following, no Workspace config.
- Every valid version enters the index. Two files claiming the same logical coordinate with different content is a hard failure.
- A directory with no supported package is rejected in default mode.
--pigstyaccepts an empty package set so rerunning an interrupted cleanup can finish and write its marker.
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:
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:
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.
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:
- Delete DEB packages whose parsed architecture is
i386. RPMs are not removed merely for carrying ani386/i486/i586/i686architecture. - Delete RPM/DEB whose binary package name is exactly
patroniand whose upstream version is exactly3.0.4. RPM comparesVERSION, ignoring epoch and release; DEB strips epoch and Debian revision first.3.0.4+foois not a match. - After all indexes render successfully, write
repo_complete: the SHA-256 of every remaining top-level RPM/DEB, sorted by basename byte order, formatted<sha256><two spaces><basename>.
Cleanup 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.
--overwriterequires--sign-withand switches torpm --resignover 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
--pigstycleanup, andrpmmust be onPATH.
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:
Machine-readable result:
Build a Pigsty flat repository with eight workers and a publication time chosen from its history:
The caller records this time and signs the resulting repomd.xml when clients require it.
Failure envelope:
Exit codes
| Code | Trigger |
|---|---|
0 |
Indexes written, or unchanged input produced a no-op |
1 |
Directory unreadable or missing, package parse failure, renderer failure, signing tool failure |
2 |
Usage error — 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
- Plain Flat Repositories — the design behind
create - Quick Start — five-minute flat repository walkthrough
- Repository Layout — what the flat tree looks like
- Sign Your Repository — generating and using a signing key
- Migrate an Existing YUM Repository — replacing an existing metadata generator
5.2 - sow init
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
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/ 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.
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:
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:
Initialize a directory other than the current one:
Initialize from a config file under version control:
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
- First Workspace — the ten-minute guided version
- Managed Workspaces — the three-layer model
- sow.yml Reference — every configuration key
- sow repo and sow dist
- Repository Layout — what
.sow/contains
5.3 - sow config
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
Parses and validates the complete sow.yml: schema version, names, path collisions, the architecture
permit list, Dist formats, membership policy and signing key references. It reports the Workspace it
resolved and how much it validated.
Options
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
--json |
Output JSON | false |
-h, --help |
Show help | — |
Strict field rejection
Unknown keys produce a configuration error:
Use schema: sow/v3:
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.
Compare that with the file on disk, which carries only what you wrote:
show filled in protected: false, the inherited per-Dist architectures, limit: 0 and an empty
exclude list. Architectures 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”:
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:
Diff effective policy across two Dists:
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
- sow.yml Reference — every key, with a complete example file
- Membership Policy — how
excludeandlimitare evaluated - Signing Model — key reference grammar and the two trust chains
- sow init — create objects declared in a hand-written config
- sow check — the runtime counterpart that verifies bytes on disk
5.4 - sow repo
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
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 creates each Repository at <workspace>/<NAME>/.
sow repo ls
Read-only listing of every Repository in the Workspace.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
--json |
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.
| 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.
| 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 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.
- Stop Workspace writers.
- Back up the whole Workspace, preserving hard links.
- Migrate each configured Repository.
- Run
buildandcheckfor each Repository.
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:
| 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:
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:
Examples
Create the Repositories for a two-tier layout:
Create a Repository without waiting for a held lock:
Print each Repository’s name, state, and 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
- sow dist — the layer below
- Managed Workspaces — the three-layer model and discovery rules
- Transactions & Recovery — lock scopes and the
recoveringstate - sow.yml Reference —
protectedand per-Repository signing - Repository Layout — the fixed directory structure
5.5 - sow dist
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
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.
DESIRED and BUILT are membership counts. When they diverge, DIRTY_REASONS says why:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
--json |
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:
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.
| 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:
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:
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.
| 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 rm
Removes a Dist’s Membership and derived indexes.
| Flag | Description | Default |
|---|---|---|
-f, --force |
Remove membership and indexes but retain pool packages | false |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Output JSON | false |
Dist removal does not delete Pool bytes
SOW removes the Dist’s configuration, membership, and indexes. Package files remain in pool/:
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:
Create a beta Dist, set its package-retention policy in sow.yml, then rebuild it:
List Dists that need a build:
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
- sow repo — the layer above
- Pool & Architecture Views — how one Dist becomes several client-visible views
- Membership Policy — configuring
limitandexclude - sow build — rebuild after a policy edit
- Repository Layout — the RPM and DEB
dists/structures
5.6 - sow add
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
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.
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:
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:
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.
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:
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:
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.
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:
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
- sow rm — the inverse operation
- sow build — build after
--skip - Membership Policy —
excluderules andlimitsemantics in full - Signing Model — the two trust chains and key references
- Package References — the coordinate grammar printed on each item line
5.7 - sow rm
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
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:
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:
Default behavior: remove and rebuild
Without --check or --skip, rm commits the Desired change and rebuilds every affected Dist
before returning. Pool objects stay on disk.
The summary is followed by one change line per affected file (eight in this run). Add --json to
receive the same result as a stable standard envelope.
Removing the last member of a Dist is fine. SOW still renders a valid, signed-if-configured empty
index — an empty Packages with a verifiable InRelease, or empty per-architecture repodata/.
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.
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:
–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.
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:
Remove one exact object from two Dists with one command:
Batch several removals, then rebuild once:
Feed the preview plan to another tool:
Exit codes
| Code | Trigger |
|---|---|
0 |
Memberships removed and rebuilt, or a --check preview printed |
1 |
Runtime I/O or renderer failure |
2 |
Usage error — --check with --skip, --check with lock flags, ambiguous selection, Workspace not found |
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
- Package References — the full reference grammar
sow ls,show, andwhere— finding the exact reference to remove- sow add — the inverse operation
- sow build — build after
--skip - Membership Policy — why removed members do not come back
sow retainandsow gc— when bytes can be reclaimed
5.8 - sow ls
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
| 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
| 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>/.
With --json, the same rows appear in result.packages.
Examples
List exact references for all objects not yet built:
List pool paths in deterministic order:
Exit codes
| Code | Trigger |
|---|---|
0 |
Membership printed, including an empty list |
1 |
Runtime I/O failure |
2 |
Usage error, Workspace not found, or implicit Repository/Dist selection is ambiguous |
5 |
Repository state database unreadable or inconsistent |
6 |
Explicit Repository or Dist is not configured |
See also
sow show— inspect one listed objectsow where— locate an object across the Workspacesow rm— remove a listed reference from Desired Membership- Package References — accepted identity forms
5.9 - sow show
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
| 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:
Copy an exact coordinate or SHA-256 from the error or from sow ls and retry.
Output
Without --json, show prints the identity, storage path, and Desired/Built locations in a compact
human-readable form:
Adding --json returns the complete Package Object under result.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 Membershipsow where— search across Repositoriessow rm— remove matching Desired Membership- JSON Output — complete result schema
5.10 - sow where
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
| 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:
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:
Example
List the Dists that include this package in Desired state:
Exit codes
| Code | Trigger |
|---|---|
0 |
One resolved Package Object and its locations printed |
1 |
Runtime I/O failure |
2 |
Usage error or Workspace not found |
5 |
A Repository state database is unreadable or inconsistent |
6 |
Explicit Repository/Dist is not configured, or the reference matched nothing/was ambiguous |
See also
sow show— inspect the resolved Package Objectsow ls— list one Dist set- Package References — exact grammar and ambiguity rules
5.11 - sow status
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
| 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
The human line reports:
- Repository state and
ready_to_copy; - Desired Revision and current Built Generation;
- affected Dists;
- pending object count and bytes;
- write-lock state.
The JSON result additionally includes dirty_reasons and the most recent Operation:
ready_to_copy 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
sow build— build pending Repository changessow check— validate the repository before deliverysow log— inspect the recent Operation reported here- Transactions & Recovery — state transitions and pointer safety
5.12 - sow build
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
| 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:
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:
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; writesrepomd.xml.ascwhen configured. - DEB: always writes
Release; writesInReleaseandRelease.gpgwhen 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
sow status— determine whether a build is neededsow check— verify the resulting treesow changes— inspect the resulting file delta- Transactions & Recovery — full commit protocol
5.13 - sow check
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
| 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.
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:
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
sow status— cheap state querysow build— build indexes from Desired state- Exit Codes — why a dirty Repository maps to
5 - Observability & Audit — operating checks and audit together
5.14 - sow changes
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
| 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
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.
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:
Filter one Dist by path after producing the Repository-level plan:
Exit codes
| Code | Trigger |
|---|---|
0 |
Plan printed, including an empty plan |
1 |
Runtime I/O failure |
2 |
Usage error, -d supplied, Workspace not found, or implicit Repository selection is ambiguous |
5 |
Repository is recovering or error, or state evidence is inconsistent |
6 |
Explicit Repository is not configured, or Base Generation is outside the valid range |
See also
sow build— create the next Generationsow publish— apply the supported publication protocolsow log— operation records and their file changes- Repository Layout — public and private path boundaries
5.15 - sow publish
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
| 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:
- package files;
- metadata with content-specific URLs;
- client entry points, such as
repomd.xmlandRelease; - 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.
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:
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
sow statusandsow check— decide whether the Built Generation is the one you intend to deliversow gc— reclaim unused target filessow.ymltargets — provider configuration- Publication Model — phases, receipts, recovery, and cache grace
5.16 - sow retain
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
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.
Adding the same Generation again changes nothing if its retained record still passes verification.
retain ls
Lists retained records for the Repository.
An empty list is a successful result.
retain rm
Removes the retention record for a Generation:
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:
- Stop Workspace writers.
- Move
.sow/<repo>/retained/<GENERATION>to a backup location outside the Workspace. - Run
sow checkagain.
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
sow gc— reclaim packages that no retained state needssow changes— inspect a Built Generation delta- Repository Layout — private retained-record location
5.17 - sow gc
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
| 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
retainrecords; - 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.
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 |
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
sow retain— protect package files from local GCsow publish— create target checkpoints and receipts- Publication Model — provider guarantees and cache grace
5.18 - sow export
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
| 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
The destination contains:
- RPM metadata with local package paths;
- the required package subtree;
- an export manifest;
.sow-export.jsonprovenance.
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.
Copy versus hard link
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
- Platforms & Integrations — client and tool compatibility
- Repository Layout — source tree and export boundaries
sow publish— managed delivery to configured targets
5.19 - sow log
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
With no argument, prints the 50 most recent Operations, newest first.
Output excerpt, one Operation object from the operations array:
payload_json records the intent — including config_sha256, the digest of the configuration in
force, and manifest_sha256 for the resulting Generation. result_json records the outcome. A
failed Operation additionally carries error_class and error_message:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Show only Operations touching this Dist | all |
--json |
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.
Output excerpt:
The files array uses the same phase vocabulary as
sow changes: payload, metadata, pointer, delete.
Build Operations also contain progress events. They keep the current state and put a
versioned object in detail_json:
The phases are rendering, promoting_payload, publishing_dists, normalizing_public_tree, and finalizing. 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:
Operation lifecycle
The state field shows how far an operation has progressed:
| 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.
Omit FILE, or pass -, to write to stdout:
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Export only Operations touching this Dist | all |
export has no --json; JSONL is its output format.
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.
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.
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:
Find failed operations among the 50 most recent records:
Archive records, then remove those older than a chosen date:
Which Operation last touched a Dist:
Exit codes
| Command | Code | Trigger |
|---|---|---|
log |
0 |
Records printed, including an empty ledger |
log |
2 |
Usage error (including a non-numeric Operation ID), Workspace not found, or ambiguous selection |
log |
5 |
State database unreadable |
log |
6 |
The given Operation ID does not exist, 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
- Observability & Audit — how
logfits withstatus,checkandchanges - Transactions & Recovery — the journal that the log records
- sow changes — compare files between builds
- JSON Output — the full log result schema