sow add
sow add is the main write path. It parses the packages you point at, derives their format and
architecture from the package headers, applies the Dist’s membership policy, and — unless you pass
--skip — rebuilds every affected index before it returns. When the command exits 0, clients can
already see the new packages.
Synopsis
Options
| Flag | Description | Default |
|---|---|---|
-R, --recursive |
Descend into subdirectories of a PATH directory |
off (top level only) |
--skip |
Update Desired state only; do not build | off |
-j, --jobs N |
Parallel workers for parsing, hashing and rendering | logical CPU count |
-C, --workdir DIR |
Workspace discovery start directory | current directory |
-r, --repo NAME |
Select a repository | selection rules |
-d, --dist NAME |
Select a distribution; repeatable | selection rules |
-T, --timeout DUR |
Maximum lock wait; 0 waits indefinitely |
0 |
-N, --no-wait |
Fail immediately when the lock is held | false |
--json |
Emit the versioned JSON envelope | false |
Inputs and targets
PATH can be a file or a directory. A directory is scanned top level only unless you pass -R.
You must end up with exactly one Repository and at least one target Dist — see the selection rules. Mixed RPM/DEB batches are fine: each package is only considered for target Dists of its own format. A package with no compatible target fails.
SOW never infers the target from a manifest, a directory name, or the host OS.
The summary line reports the Operation ID, per-item counts, the membership delta, the new Desired
Revision, the Built Generation and whether the Repository is left dirty. Then one item line per
input, in stable order.
Item statuses
Each item line carries an overall status plus a per-Dist verdict in dists=.
| Status | Meaning |
|---|---|
accepted |
New Package Object created and at least one Membership added |
reused |
The content already exists in this Repository; only Membership references may change |
excluded |
Policy removed it from every target Dist — see the dists= field for excluded vs limited |
failed |
The package was rejected; the error= field says why |
reused is content idempotence: adding the same file twice never creates a second object or a
duplicate Membership. A default repeated add also converges the selected Dist. If it is already
current, the Generation stays put; if an earlier --skip or configuration change left it dirty,
the reused input triggers the missing build and may advance the Generation:
The same object added to a second Dist is also reused — the pool keeps one copy and gains a second
Membership. Use --skip again when the intent is to retain a dirty batch instead of converging it.
Architecture is read, never guessed
add reads format and native architecture from the package header, then checks the Workspace permit
list. An architecture outside the list fails the package and tells you exactly what to edit:
It does not create a directory and does not modify sow.yml.
RPM noarch and DEB all are architecture-neutral. They create one Package Object and one
Membership, and render into every effective architecture view of the target Dist. They do not
spread to Dists you did not select with -d.
Policy: exclude and limit
After merging into the target Memberships, SOW re-evaluates exclude and then limit over the
complete Dist candidate set. A package removed by policy is reported, not treated as a parse failure.
Here trixielim has exclude: [{kind: [dbgsym]}] and limit: 1. The dbgsym package was excluded by
rule; libpq5 18.2-1 lost to 18.3-1 under the version limit and is reported as limited. Both
appear as excluded in the top-level status, and the dists= field distinguishes them.
Limit groups by (binary name, native architecture), so 18.3-1:amd64 and 18.3-1:arm64 both
survive a limit: 1. A package can be accepted by one Dist and skipped by another in the same run.
exclude and limit remove real Desired Memberships. Relaxing the policy later does not resurrect
them — leftover bytes in pool/ are not a candidate set. Re-run sow add.
Partial batches
Valid, conflict-free packages are committed even when siblings fail. Failed inputs stay where they
are, each with its own error, and the command exits 3:
If nothing is accepted, the whole operation is rejected with exit 6 and the Repository is
unchanged:
There is no rejected/quarantine directory.
–skip
--skip stops after the Desired state is committed. The public pool/ and dists/ bytes do not
change, the Built Generation stays where it was, and the Repository becomes dirty. New package bytes
are durably held in a private pending store until the next build publishes them.
pending=4/2326 is four objects totalling 2326 bytes waiting in the private store. They never appear
in sow changes — only a successful build promotes them into the
deliverable tree.
Use --skip for bulk imports, then converge once:
Processing order
For the record, one add executes in this order:
- Take the Repository write lock and recover any unfinished Operation.
- Commit a
plannedOperation in SQLite. - Parse inputs read-only; compute logical coordinates and input SHA-256 (plus, for RPM, the signature-neutral payload digest).
- Check the architecture permit list and look up existing coordinates.
- For genuinely new coordinates only, run optional RPM signing on a stage copy and compute the final SHA-256, then verify content and path uniqueness.
- Merge target Memberships, then apply
excludeandlimitover the complete Dist set. - Commit the Desired state; new bytes go to the private pending content store.
- Unless
--skip, publish still-needed pending objects intopool/and render indexes — each Dist is built at most once per command.
Input files are never modified, moved or deleted, in any mode.
RPM signing modes
Managed RPM package signing is configured in sow.yml under signing.rpm.packages.mode; there is no
command-line override.
| Mode | Behavior |
|---|---|
never |
Keep the input bytes exactly |
fill |
Sign when unsigned or when the signature is not trusted; keep bytes when a trusted_keys signature verifies. Default when a key is configured |
always |
Ensure the final package is validly signed by the configured key; re-sign a stage copy otherwise |
Without a configured key only never is available.
Because a signature embeds non-deterministic fields, SOW cannot re-sign and then compare final
hashes. Retry idempotence works on the coordinate instead: identical input bytes are reused
directly; an identical RPM signature-neutral digest is reused when the existing object satisfies the
current policy. A different payload digest, or an existing object that no longer satisfies the
policy, is a hard conflict — add will not silently re-sign a coordinate in place.
Exit codes
| Code | Trigger |
|---|---|
0 |
Every input accepted or reused; indexes rebuilt (or skipped with --skip) |
1 |
Runtime I/O, parser, renderer or signing failure |
2 |
Usage error, Workspace not found, or ambiguous Repository/Dist selection |
3 |
Partial batch — at least one item committed and at least one failed |
4 |
Repository lock held and --no-wait given or --timeout expired |
5 |
Integrity or recovery error, including a build that failed after applied |
6 |
Nothing accepted — unsupported architecture, no compatible target Dist, or a coordinate conflict |
See also
- sow rm — the inverse operation
- sow build — converging after
--skip - Membership Policy —
excluderules andlimitsemantics in full - Signing Model — the two trust chains and key references
- Package References — the coordinate grammar printed on each item line