sow.yml Reference

Every field of the workspace configuration file, with validation rules and a complete worked example.

sow.yml is the single configuration file of a managed workspace. It sits at the workspace root, declares which repositories and distributions exist, and holds the membership policy and signing settings that every build applies. Plain mode (sow create) never reads it.

This page lists every field the parser accepts. Anything not listed here is rejected — there are no undocumented keys and no keys reserved for future use.

How the file is read

SOW parses sow.yml with a strict decoder. Practically, that means:

  • Unknown fields are errors, not warnings. A typo like repositories: instead of repos: fails the command with exit code 2 and names the offending line.
  • Exactly one YAML document. A --- separator introducing a second document is an error.
  • Regular file only. A symlink at sow.yml, or a file larger than 16 MiB, is rejected before parsing.
  • Defaults are filled in at parse time, not written back to disk. Run sow config show --all to see the fully expanded form.

Some of the file is machine-maintained. sow init, sow repo new, sow repo rm, sow dist new, and sow dist rm rewrite sow.yml atomically as part of their transaction. Membership policy and signing are yours to edit by hand; there are no CLI flags that set them.

After any hand edit, run sow config check. It parses the file, cross-checks it against the SQLite state of every initialized repository, and resolves every signing key reference — without writing anything.

sow config check
configuration valid: /srv/repo repositories=1 dists=2

Top level

schema: sow/v2
architectures: [x86_64, aarch64]
repos:
  <name>: <repository>
FieldTypeRequiredDefaultMeaning
schemastringyesMust be exactly sow/v2. Any other value is a configuration error.
architectureslist of stringsno[x86_64, aarch64]The CPU families this workspace is allowed to manage.
reposmapnoemptyRepository name to repository configuration.

architectures

This is a ceiling, not a target. It declares which architectures SOW may accept at all; individual distributions inherit the whole list unless they narrow it.

Only two canonical families are supported today: x86_64 and aarch64. The DEB ecosystem names are accepted as input aliases and normalized at the parse boundary:

You may writeStored and displayed as
x86_64, amd64x86_64
aarch64, arm64aarch64

So architectures: [amd64, arm64] and architectures: [x86_64, aarch64] are the same configuration. Writing both aliases of one family — [amd64, x86_64] — is a duplicate and fails.

noarch (RPM) and all (DEB) are not architectures here. They are neutral packages, projected into every applicable view at build time, and the parser rejects them in this list. An unsupported value such as riscv64 fails immediately:

configuration error: load config "/srv/repo/sow.yml": workspace architectures: unsupported architecture "riscv64"; supported canonical families are x86_64 and aarch64

The list may be present or absent, but it may not be empty.

Repository

repos:
  pigsty:
    protected: true
    signing: { ... }
    dists: { ... }
FieldTypeRequiredDefaultMeaning
protectedboolnofalseWhen true, sow repo rm refuses to delete this repository, even with -f.
signingmapnononePackage and metadata signing settings, see Signing.
distsmapnoemptyDistribution name to distribution configuration.

protected

protected: true is a guard against deleting a whole repository by accident. It blocks exactly one thing — repository removal:

operation rejected: managed: operation rejected: repository "pigsty" is protected

That is exit code 6. Everything else keeps working normally: you can still add, rm, build, create and delete distributions. To actually remove a protected repository, edit sow.yml to set protected: false, confirm with sow config check, then run sow repo rm.

Repository names

Repository and distribution names share one grammar: they must match [a-z0-9][a-z0-9._-]* — lowercase letters, digits, dot, underscore, hyphen, starting with a letter or digit. Uppercase is rejected, because the name becomes a directory and must behave identically on case-sensitive Linux and case-insensitive macOS filesystems.

configuration error: load config "/srv/repo/sow.yml": repository name "Infra": name "Infra" must match [a-z0-9][a-z0-9._-]*

These names are reserved and rejected: ., .., .sow, pool, dists, sow.yml, workspace.lock, workspace-ops, repo-locks. Two repository names that would collide in the state directory — say db and db.db — are also rejected. See Repository Layout for why.

Dist

    dists:
      el9:
        format: rpm
        architectures: [x86_64]
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]
FieldTypeRequiredDefaultMeaning
formatstringyesrpm or deb. A distribution holds exactly one format.
architectureslist of stringsnoinherits workspace listNarrows this distribution to a subset of the workspace families.
limitintegerno0Maximum versions to keep per package name and architecture; 0 keeps all.
excludelist of rulesnoemptyRules that keep matching packages out of this distribution.

format

format is the only field sow dist new sets from the command line, and it cannot be changed afterwards — an RPM distribution never becomes a DEB one. A package whose format does not match is simply not a candidate for that distribution.

configuration error: load config "/srv/repo/sow.yml": repository "a" dist "d1" format must be rpm or deb, got "apk"

architectures

Omit this field and the distribution inherits the workspace list, which is what you want almost always. Declare it only to narrow: an el9 distribution that should be x86-only in an otherwise dual-architecture workspace.

The list must be a subset of the workspace list, and it may not be empty:

configuration error: load config "/srv/repo/sow.yml": repository "a" dist "d1" architecture "aarch64" is not allowed by workspace

Adding a family here marks the distribution dirty; the next sow build renders the new view. Removing a family that is still referenced by existing membership or by the built generation is refused by config check and by every write command.

limit

limit caps how many versions of one package survive in this distribution. The grouping key is (binary name, native architecture), so an x86_64 build and an aarch64 build of the same package are counted separately, and a noarch/all package forms its own group.

  • 0 (the default) keeps every version.
  • N > 0 keeps the newest N, comparing RPM EVR or Debian version with the native ordering rules of each ecosystem.
  • A negative value is a configuration error.
configuration error: load config "/srv/repo/sow.yml": repository "a" dist "d1" policy: limit must be zero or positive, got -1

With limit: 1, adding an older version alongside a newer one reports it as limited and does not create membership:

item input=".../libpq5_18.2-1.pgdg12+1_amd64.deb" status=excluded format=deb coordinate="libpq5=18.2-1.pgdg12+1:amd64" sha256:310611d0... dists=trixie:limited
item input=".../libpq5_18.3-1.pgdg12+1_amd64.deb" status=accepted format=deb coordinate="libpq5=18.3-1.pgdg12+1:amd64" sha256:4b526223... dists=trixie:accepted

Raising limit later does not resurrect versions that policy previously removed. The package bytes may still sit in the pool, but membership is gone; re-add the file to bring it back. See Membership Policy for the reasoning.

exclude

exclude is a list of rules. Each rule is a set of fields; within a rule the fields are ANDed, within a field the patterns are ORed, and rules are ORed with each other. A package is excluded if any single rule matches it.

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

That reads: drop every debug-flavored package, and separately drop aarch64 packages whose name starts with test- or ends with -experimental.

Five fields are allowed:

FieldMatched against
nameBinary package name
sourceNormalized source name (RPM SOURCERPM, DEB Source)
archx86_64, aarch64, or neutral
kindThe classification below
formatrpm or deb

kind is derived from the binary package name by its most specific suffix:

FormatName suffixkind
RPM-debuginfodebuginfo
RPM-debugsourcedebugsource
RPM-llvmjitllvmjit
DEB-dbgsymdbgsym
DEB-dbgdbg
anynone of the abovemain

Patterns are case-sensitive: either an exact string or a shell glob using *, ?, and [...]. There is no regex, no version comparison, no negation, and no expression syntax. An empty rule, an empty or untrimmed pattern, a repeated pattern within one field, and an invalid glob are all configuration errors:

configuration error: load config "/srv/repo/sow.yml": repository "a" dist "d1" policy: exclude rule 0 is empty
configuration error: load config "/srv/repo/sow.yml": repository "a" dist "d1" policy: exclude rule 0 field name has invalid glob "[bad": syntax error in pattern

Policy order is fixed: exclude runs first, then limit. An excluded package is reported per item and is not a failure:

item input=".../blackbox_exporter-0.28.0-1.x86_64.rpm" status=excluded format=rpm coordinate="blackbox_exporter-0:0.28.0-1.x86_64" sha256:5759c643... dists=el9:excluded

Signing

Signing settings live on the repository, not on individual distributions, and cover two independent trust chains: the packages themselves, and the repository metadata clients verify before they trust anything else.

    signing:
      rpm:
        packages:
          mode: fill
          key: env://SOW_RPM_PACKAGE_KEY
          trusted_keys: [keys/pgdg.asc]
        metadata:
          key: keys/repo-signing.asc
          passphrase: env://SOW_METADATA_PASSPHRASE
      deb:
        metadata:
          key: keys/repo-signing.asc
          passphrase: env://SOW_METADATA_PASSPHRASE

The tree is fixed. signing.rpm has packages and metadata; signing.deb has metadata only — DEB packages are never re-signed, because APT verifies the archive through Release, not through per-package signatures.

rpm.packages

FieldTypeDefaultMeaning
modestringnever, or fill when key is setnever, fill, or always.
keykey referencenoneThe signing key. Required unless mode is never.
trusted_keyslist of key referencesemptyAdditional public keys accepted by fill.

The three modes:

  • never — input bytes are stored verbatim. Whatever signature the package arrived with (including none) is what clients get.
  • fill — sign packages that have no signature, or whose signature is not verifiable by key or one of trusted_keys. Packages that already verify keep their exact bytes.
  • always — every package must end up signed by key. Packages already signed by it keep their bytes; everything else is re-signed.

fill is the default when a key is present, because it is the mode that preserves upstream signatures. Setting mode to fill or always without a key is an error:

configuration error: load config "/srv/repo/sow.yml": repository "a" signing: rpm packages mode "fill" requires key

trusted_keys is a list of public keys whose signatures fill accepts as already-good. The public half of key is always trusted and does not need to be listed. Repeating the same reference twice is an error.

RPM package signing is the one operation that shells out: SOW calls the environment’s rpm --addsign / rpm --resign against a private staged copy, never against your input file. The private key must be available to the GPG environment that rpm uses.

rpm.metadata and deb.metadata

FieldTypeDefaultMeaning
keykey referencenoneKey used to sign repository metadata.
passphrasepassphrase referencenonePassphrase for a protected private key.

Configure rpm.metadata.key and every RPM architecture view additionally publishes a detached repodata/repomd.xml.asc. Configure deb.metadata.key and every DEB distribution additionally publishes a clearsigned InRelease and a detached Release.gpg. Without a key, those files are simply not produced — repomd.xml and Release are always written.

For file:// and env:// references SOW signs in-process; no gpg binary is involved. Only agent:// requires gpg in the environment.

Changing a key reference or the fingerprint behind it marks the affected distributions dirty, because the signing identity is part of each distribution’s built configuration digest. The next sow build re-signs and produces a new generation.

Key references

A key reference is a string in one of these forms:

FormExampleNotes
Pathkeys/repo-signing.ascASCII-armored key file. A relative path resolves against the workspace root, not your current directory.
file://<path>file:///secure/repo-signing.ascSame as above, written explicitly. Absolute paths therefore show three slashes.
env://<VAR>env://SOW_METADATA_KEYThe variable holds the armored key material itself, not a path. The name must match [A-Za-z_][A-Za-z0-9_]*.
agent://<fingerprint>agent://7F721C4AD40F...CF3BDelegates to the ambient gpg-agent. The fingerprint is 16, 40, or 64 hex digits, case-insensitive.

Any other scheme is rejected:

configuration error: load config "/srv/repo/sow.yml": repository "a" signing: deb metadata key: unsupported key reference scheme in "https://example.com/key.asc"

References are validated in two stages. Syntax is checked when the file is parsed and fails with exit code 2. Whether the reference actually resolves is checked by sow config check and by every write command, and fails with exit code 6:

operation rejected: ... deb metadata key: key reference does not resolve to a bounded regular file
operation rejected: ... deb metadata key: environment key reference SOW_METADATA_KEY is unset
operation rejected: ... deb metadata key: gpg public-key export returned no bounded key material

Secret material never leaves the reference. sow config show --all prints the reference and the resolved fingerprint, and nothing else:

    signing:
      deb:
        metadata:
          key: file:///srv/repo/keys/repo-signing.asc
          key_fingerprint: 7F721C4AD40F4A9D8CA578BFAC7E4690B50CCF3B

Private keys and passphrases are never written to sow.yml, SQLite, the operation log, JSON output, or error messages.

Passphrase references

passphrase accepts the same path, file://, and env:// forms as a key reference — but not agent://, since a passphrase is a value, not a key handle.

Two rules apply:

  • A passphrase without a key is an error. It has nothing to unlock.

    configuration error: ... repository "a" signing: deb metadata passphrase requires key
    
  • A passphrase alongside an agent:// key is an error. The agent owns the private key and handles its own prompting; a second passphrase channel would be ignored.

    configuration error: ... repository "a" signing: rpm metadata agent key uses its ambient gpg-agent and cannot accept a passphrase reference
    

Complete example

A workspace with two repositories: a protected production repository signing both metadata chains and filling in missing RPM signatures, and a scratch repository with no signing and no policy.

# sow.yml — workspace root configuration
schema: sow/v2

# CPU families this workspace may manage. This is a ceiling, not a target.
# amd64/arm64 are accepted as input aliases and normalized to x86_64/aarch64.
architectures: [x86_64, aarch64]

repos:

  # Production repository. Deleting it requires editing this file first.
  pigsty:
    protected: true

    signing:
      rpm:
        packages:
          # Sign RPMs that arrive unsigned or with an untrusted signature.
          # Packages already signed by a trusted key keep their exact bytes.
          mode: fill
          key: keys/package-signing.asc
          trusted_keys:
            - keys/pgdg.asc        # upstream PGDG signatures are accepted as-is
        metadata:
          # Publishes repodata/repomd.xml.asc next to every repomd.xml.
          key: keys/repo-signing.asc
          passphrase: env://SOW_METADATA_PASSPHRASE
      deb:
        metadata:
          # Publishes InRelease and Release.gpg next to every Release.
          key: keys/repo-signing.asc
          passphrase: env://SOW_METADATA_PASSPHRASE

    dists:

      # Stable EL9 channel: one version per package, no debug artifacts.
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]

      # Beta channel: same packages, every version kept for rollback.
      el9-beta:
        format: rpm
        limit: 0
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]

      # Debian trixie, x86 only, newest version wins.
      trixie:
        format: deb
        architectures: [x86_64]
        limit: 1
        exclude:
          - kind: [dbgsym, dbg]
          - name: ["*-experimental"]

  # Scratch repository: unsigned, unfiltered, deletable.
  sandbox:
    dists:
      el9:
        format: rpm
      trixie:
        format: deb

Validate it before you rely on it:

sow config check
sow config show --all

What is not in sow.yml

Some things you might expect to configure are deliberately not configurable:

  • Repository paths. A repository always lives at <workspace>/<name>. There is no path: field. See Repository Layout.
  • APT components. Always main. YUM has no component concept.
  • Architecture views. Derived from architectures and the package headers, never declared per package.
  • Remote endpoints, mirrors, credentials. SOW writes a local directory tree and stops there; copying it anywhere is your job and your tool’s.
  • Anything about generations or retention counts. The retention window is fixed by the transaction model, not tunable.

See also

Last modified: 2026-08-08: init commit (fe725aa)