Configuration¶
zoryn reads configuration from three places, in order of specificity:
| File | Purpose |
|---|---|
~/.zoryn | Global user config (TOML). Required for zoryn to work. |
~/.config/zoryn/builders.d/*.conf | One .conf per builder — local or remote hasher machine. |
.gear/version-up | Per-package overrides: upstream version mapping, CVE sources, merge hints. |
Run zoryn gen environment to bootstrap ~/.zoryn plus SSH, hasher and GPG configs in one shot.
~/.zoryn¶
Minimal example — zoryn works with just this:
All other sections have sensible defaults. Override only what genuinely differs for you.
Full example¶
[build]
command = "hsh -v --number={hasher_number} --mountpoint=/proc,/dev/pts,/dev/kvm --lazy-cleanup {hasher_dir}"
log_filename = "build.{batch}.{builder}.log"
[builders]
default = "local"
default_arch = "x86_64"
parallel = "off"
results_download_dir = "{git_root}/hasher_out"
batch_repo = "~/zoryn-batch-repo"
[gitery]
host = "gitery"
remote = "gitery"
login = "rider"
[gitoskop]
url = "https://git.altlinux.org/gitoskop/api"
[gyle]
host = "gyle"
[sources]
srpms_path = "/mnt/ftp/pub/distributions/ALTLinux/Sisyphus/files/SRPMS/"
[rebuild]
command = "hsh -v --mountpoint=/proc,/dev/pts,/dev/kvm --lazy-cleanup"
log_dir = "/tmp/rebuild-logs"
[add_changelog]
up_template = "- {old_version} -> {new_version} {cves}"
[ssh]
multiplexing = true # reuse TCP connections via ControlMaster (default: true)
persist = "10m" # keep master connection alive after last use (default: 10m)
[notify]
enabled = true # desktop notifications for long-running commands (default: true)
[commands]
# Optional: override paths and arguments for external commands
# git = "/usr/local/bin/git"
# git.fetch = "{git} fetch --prune"
# ssh = "ssh -o ConnectTimeout=10"
# ssh.gitery = "{ssh} -p 2222 -i ~/.ssh/alt_key"
Section reference¶
[build]¶
command— hsh command for local builds.zoryn gen environmentwriteshsh -v --packager={packager} --mountpoint=/proc,/dev/pts,/dev/kvm --lazy-cleanup {hasher_dir}. The{hasher_dir}argument keeps builds and other local-builder operations in the same directory (~/hasherby default). Usehsh, notgear-hsh— the tarball is created separately withgear --commit. For parallel builds with multiple local hashers, also use{hasher_number}.log_filename— log filename template (default:build.{batch}.{builder}.log). Placeholders:{builder},{batch},{pkgname}.packager— packager string (Name <email>) passed tohsh --packager=for local hasher builds (default: unset — hsh uses its own configured packager).
[builders]¶
default— default builder(s) when no--builderis specified and no per-branch override applies. Comma-separated string or TOML array (e.g."local"or["local", "arm-server"]).default_arch— default architecture(s) for multi-builder mode. Comma-separated string or TOML array. When multiple architectures are listed, multi-builder mode is enabled automatically.parallel— default parallel mode for multi-builder builds (onoroff, default:off). Override with--parallel/--sequential.results_download_dir— directory for downloading remote build results (default:{git_root}/hasher_out). Supports{git_root}.batch_repo— directory for accumulating RPMs during batch builds. Used bydownload_rpms/upload_rpms.repo_workdir— root directory under whichzoryn task mkrepowrites generated task repositories (<repo_workdir>/<task_id>/repo). Can be set globally here or per-builder.
[builders.<branch>] — per-branch defaults¶
[builders]
default = "sis-x86, sis-arm"
default_arch = "x86_64"
[builders.p11]
default = "p11-x86"
default_arch = "x86_64"
[gitery]¶
host— SSH alias for gitery. Must match an entry in~/.ssh/config.remote— name of the git remote pointing to gitery, used for push operations.login— gitery/ALT Linux login, used byzoryn giteryto resolve a bare repository name topeople/<login>/packages/<name>. Required for bare-name resolution; unused when a full path or-n/--namespaceis given. When this key is unset, the login is derived from your gituser.emailif it is an@altlinux.orgaddress — its local part (before the@) becomes the login.
[gitoskop]¶
url— gitoskop HTTP API base URL, used by the read commands ofzoryn giteryand as the base of the built-ingitoskopMCP server ofzoryn agent mcp(default:https://git.altlinux.org/gitoskop/api). A trailing slash is stripped.
[gyle]¶
host— SSH alias for the gyle build system.
[sources]¶
srpms_path— local SRPMS mirror (required forzoryn task rebuild).
[rebuild]¶
command— hasher command for rebuilding packages (default:hsh -v --mountpoint=/proc,/dev/pts,/dev/kvm --lazy-cleanup).log_dir— base directory for build logs (default:/tmp/rebuild-logs).
[tasks]¶
api_url— Tasks API base URL (default:https://git.altlinux.org/tasks/api).user— girar username for API queries (default: extracted from%packageremail in~/.rpmmacros).
[rdb]¶
api_url— RDB API base URL (default:https://rdb.altlinux.org/api).
[clone]¶
api_url— API endpoint that returns a package's git clone URL, used byzoryn clone(default:{[rdb] api_url}/package/clone_url). A trailing slash is stripped.
[repoteka]¶
url— Repoteka base URL for package/arch queries (default:https://rdb.altlinux.org/repoteka). A trailing slash is stripped.
[hosts] — builder host monitoring limits¶
Load thresholds checked before dispatching a build to a host. Set them globally in [hosts], or per-host in [hosts."<hostname>"] (a host-specific value wins over the global one).
min_free_ram— minimum free RAM before the host is considered busy (default:2G). Accepts suffixes such as2G,512M.max_load_avg— maximum 1-minute load average (default:8.0).max_io_wait— maximum I/O-wait percentage (default:50).
[add_changelog]¶
up_template— changelog entry template forzoryn up(default:- updated from {old_version} to {new_version} {cves}).{old_version},{new_version},{cves}(the latter expands to(Fixes: CVE-...)or empty).- The per-package
.gear/version-up[add_changelog]takes priority over~/.zoryn. - Trailing whitespace is trimmed automatically.
[submit]¶
run— whether to run a task after creating/modifying (default:false).test_only— whether to mark tasks as test-only (default:true).
To restore the pre-zoryn behaviour (run with --commit by default):
[ssh]¶
multiplexing— enable SSH connection multiplexing via OpenSSH ControlMaster (default:true). Reuses TCP connections to the same host, reducing latency for multi-step builder operations.persist— how long the master connection stays alive after the last session ends (default:10m). Uses the same time format asssh_config(5)ControlPersist.alive_interval—ServerAliveIntervalfor the multiplexed connection, in seconds (default:15).alive_count_max—ServerAliveCountMaxfor the multiplexed connection (default:3).- Socket files are stored in
$XDG_RUNTIME_DIR/zoryn/(or$TMPDIRfallback) and cleaned up automatically on exit.
[notify]¶
enabled— enable desktop notifications for long-running commands (default:true). Sends OSC 99 escape codes (kitty protocol) followed by BEL to stderr. Kitty, WezTerm and foot show a popup; other terminals play the bell. Notifications fire at completion of:build,up,task rebuild,task test-rebuild,task batch.
[commands]¶
- Override paths and global arguments for external commands (
git,ssh,gear-*,rpm, etc.). - Subcommands inherit from their base:
git.fetch = "{git} fetch --prune"— ifgitis overridden,git.fetchuses the override. ~/and$HOMEare expanded to absolute paths.- Shell metacharacters (
;,|,`,$()) are rejected at startup.
[devenv]¶
backend— default backend forzoryn devenv:bwraporpodman. When unset,podmanis used if installed, otherwisebwrap.packages— extra packages to install in the development environment on top of the spec'sBuildRequires:(e.g. debuggers, editors, profilers).image— base image for the podman backend (default:registry.altlinux.org/<branch>/alt:latest). Set this when the default is not pullable on the current host (e.g. a local mirror or private registry);zoryn devenv --image IMAGEoverrides it per invocation. Not read from.gear/devenv.build— array of shell commands run asRUNsteps when building the podman image, as root. Your steps go into the image's project-specific tail, after the shared feature layers (see features). Machine config only — not read from.gear/devenv.build_user— likebuild, but run as the host user (per-user installers, e.g.curl … | bashinto~/.local/bin); usesudofor root steps. Machine config only.mounts— array of host paths /host:container[:opts]specs bind-mounted into the podman container (each-vat container-create time, in addition to the always-mounted project directory). A bare path is bound at the same path. An entry written astmpfs:<dest>[:<options>]is not a bind at all: it becomes--tmpfs <dest>[:<options>], a fresh tmpfs inside the container with kernel tmpfs options (size=8g,mode=1777). The mount is made when the container is created, so a directory an unprivileged user has to write needsmode=1777among the options — there is nothing left to chown it afterwards. A spec may reference{devenv}(the environment's container name, as inzoryn devenv list; on bwrap, which has no container, thedevenv-<project>hostname),{project}(the project directory's basename),{branch}(the effective ALT branch) and{profile}(the active profile,defaultwhen none):mounts = ["~/.config/herdr/sessions/{devenv}"]gives every environment its own host directory. The container name embeds the configuration hash, so a change that recreates the container also points{devenv}at a fresh directory; use{project}for one that survives recreations. Unknown{tokens}are left as written. Podman backend only; machine config only — not read from.gear/devenv.pids_limit— integer pid cap for the podman container (--pids-limit; default 4096,0or a negative value for unlimited). The same value is also applied as--ulimit nproc=<n>:<n>, soulimit -uinside the container matches the cgroup cap instead of podman's much lower default (~512) — raise this key when a parallel build fails withposix_spawnp: Resource temporarily unavailable. The soft limit is never lowered below the host's and never raised above the host's hard limit. The container also runs with--initso zombie subprocesses (e.g.git's background auto-gc) are reaped. Podman backend only; machine config only.prompt— literal bashPS1string baked into the podman image's user~/.bashrc, so the interactive shell uses it. Standard prompt escapes (\u,\h,\w, colour\[\e[…m\]…\[\e[0m\]) are interpreted at prompt time. Changing it rebuilds the image. Podman backend only; machine config only — not read from.gear/devenv.outbound_interface— host network interface name (e.g.eth1). When set, the podman container is switched off--network hostonto a rootlesspastanetwork namespace whose outbound IPv4/IPv6 traffic is bound to that interface, so its outgoing connections leave the host via that interface (and host-localhost services are no longer shared). Changing it recreates the container from the cached image (likemounts, it is not part of the image tag). Podman backend only; machine config only — not read from.gear/devenv.dns,dns_search,dns_option— the three/etc/resolv.conffields, passed to podman as--dns(nameserver addresses, IPv4/IPv6 or the literalnone),--dns-search(search domains, or.to empty the list) and--dns-option(resolver options such asndots:2). Each is an array and applies to both the image build and the container, soaptresolves names at build time too. Set them when the container cannot reach the host's resolvers — typically alongsideoutbound_interface, whose pasta binding routes DNS out that interface.zoryn devenv --dns ADDR/--dns-search DOMAIN/--dns-option OPTset them for one run. Layer-select per key (likeapt_sources, unlikemounts): flag > per-projectprojects.d/<project>.toml> global~/.zoryn, the winning layer replacing the others — entry order matters, so a project must be able to put its own resolver first. Not part of the cache key: a change recreates the container (image layers are reused). Podman backend only; machine config only — not read from.gear/devenv.forward_ssh_agent— whentrue, forward the host SSH agent into the podman container (host$SSH_AUTH_SOCKbind-mounted to/run/ssh-agent.sock) so tools inside use your agent keys;zoryn devenv --ssh-agent/-Aenables it per run. Podman backend only; machine config only.ports— array of port specs published from the container to the host:"8080"maps the same port on both sides,"18080:8080"maps host port18080to container port8080, so a dev server inside is reachable athttp://localhost:18080. An entry may be prefixed with a host interface or address to publish it beyond the loopback —"eth0:8080","192.0.2.10:18080:8080","*:3000"for every interface; without a prefix the bind is127.0.0.1. Interface names are resolved to their first IPv4 address on every run; a changed address recreates the container. Overlapping binds ("8080"plus"*:8080", or two entries resolving to one address with different guests) are refused; two spellings of one mapping are published once. The container sees every client as the tap address rather than the real peer. Unlike the resolver settings, this key concatenates across layers: the project-local list adds to the global one (an entry present in both is published once; there is no per-project off-switch). Ignored whenoutbound_interfaceis unset — with--network hostthe container already shares the host's ports. Changing the list recreates the container. Podman backend only; machine config only (~/.zorynand~/.config/zoryn/projects.d/<project>.toml, never.gear/devenv).timezone— IANA zone name (e.g."Asia/Almaty") passed topodman run --tz, which setsTZand/etc/localtimeinside the container. When unset, the container keeps the image default (UTC).zoryn devenv --timezoneoverrides it for one run; changing the effective zone recreates the container. Podman backend only — bwrap inherits the hostTZ; machine config only — not read from.gear/devenv.env— array ofNAME=valueenvironment entries set in the environment's shell, e.g.env = ["GOFLAGS=-mod=vendor", "EDITOR=vim"]. With podman they are passed as--envat container creation, so the enter shell (andzoryn devenv install) inherits them; changing the list recreates the container (image layers are reused). With bwrap they become--setenvat enter time. Likeports, the key concatenates across layers: global~/.zorynfirst, then the project-local list — and since the last assignment of a name wins, a project entry overrides a global one, and both override an entry contributed by a selected feature. Each entry must be a well-formedNAME=valuepair with no control characters, no trailing space, and no braces beyond the{devenv}/{project}/{branch}/{profile}placeholders, which expand in env values exactly as inmounts—env = ["HERDR_SESSION=~/.config/herdr/sessions/{devenv}"]tells a tool inside the container its own per-environment directory. Withforward_ssh_agent, the forwardedSSH_AUTH_SOCKwins over anenventry of that name. The entries reach only the environment's shell — the podman image build (build/build_usersteps, feature installers, apt) does not see them, so a build-time proxy belongs inbuildsteps or in the apt configuration, not here. Machine config only — never read from.gear/devenv, so a package repository cannot plantLD_PRELOAD& co. into your shell.branch— target ALT branch (defaultsisyphus); also derives the default podman base image whenimageis unset.zoryn devenv --branch/-Boverrides it for one run.apt_config— host apt config path: an apt.conf-format file, asources.listfile, or a directory containing either. For the bwrap backend: normalised to an apt.conf file forhsh --apt-config(asources.listis wrapped into a generated apt.conf) so the chroot is populated from the chosen repositories. For the podman backend: used as the apt sources for the container image (replaces the base image'ssources.list); when the repositories name a different ALT branch than the effective devenv branch, the source is dropped with a warning and the base image's own sources are kept. Tilde-expanded; must exist. Machine config only.apt_builder— builder name whose apt config is reused for the podman container'ssources.list(e.g."local"). Podman backend only; machine config only.apt_sources— array of apt source lines written verbatim into the podman container'ssources.list(e.g.["rpm file:///mnt/alt x86_64 classic"]). Localfile:repositories are bind-mounted read-only at the same path; a missing host path is an error. Podman backend only; machine config only.default_profile— name of the[devenv.profiles.<name>]profile to apply when--profileis not given. Base-only — never read from within a profile.[devenv.features.<id>]— select devenv feature<id>(a reusable bundle of packages + install steps + mounts); the table's keys are the feature's options. A local definition (~/.config/zoryn/devenv/features/<id>.toml, or<id>/feature.toml) overrides the built-in. Podman only; machine config only.[devenv.lan]— filtered LAN access for the podman container:interface(host LAN interface the container may reach) andallow(array of IP addresses/CIDRs routed to that interface; everything else keeps the default route and cannot reach the LAN). A bare address means a/32host route; host names are not accepted — DNS keeps leaving through the tunnel by design. Requiresoutbound_interfaceto be set and to differ frominterface. Entries overlapping link-local (169.254.0.0/16,fe80::/10), covering the default route, or naming the host's own address are rejected. The allow-list governs only connections the container initiates: a TCP client from an allowed subnet can still reach the container's published ports, since replies are sourced from the container's own address and keep the default path (see the limitations for the UDP and bound-socket cases). Editingallowdoes not recreate the container — routes are reconciled into the running one on the nextzoryn devenv;zoryn devenv inspectshows the live routes. Machine config only — never read from.gear/devenv, so a package cannot open a hole into your LAN. Like every profile key, an active profile reads its own[devenv.profiles.<name>.lan]; the base table is not inherited.Limitations, by design:
- Image build runs in its own namespace per step, so a LAN apt mirror is unreachable at build time.
- Filtering is per destination address; every port of an allowed address is reachable.
- No name resolution over the LAN path — DNS leaves through the tunnel.
- One host interface.
Since zoryn 0.49.0 (the switch to the otoml parser) both sub-tables can also be written as TOML inline tables: features = { vim = {}, claude = {} } and lan = { interface = "eth0", allow = ["192.168.50.0/24"] } are equivalent to the section-header form. Place such inline keys inside the [devenv] block before any [devenv.*] section header — in TOML a bare key = value line belongs to the most recent section header above it.
A profile is a named, fully independent [devenv] configuration declared as [devenv.profiles.<name>]. When it is active, every [devenv] key is read only from the profile; the base [devenv] section is not inherited or merged into it (the base applies only when no profile is active). A package's own BuildRequires and committed .gear/devenv packages still apply regardless of profile. Select with zoryn devenv --profile <name>. See zoryn devenv.
[devenv]
backend = "bwrap"
packages = ["gdb", "vim", "strace"]
# image = "registry.example.com/alt/sisyphus"
# mounts = ["/srv/sources:/srv/sources:ro"]
# prompt = "\\[\\e[32m\\]\\u@\\h\\[\\e[0m\\]:\\w\\$ "
# outbound_interface = "eth1"
# timezone = "Asia/Almaty"
# dns = ["8.8.8.8"]
# dns_search = ["corp.example.com"]
# ports = ["18080:8080", "eth0:8080"]
# env = ["GOFLAGS=-mod=vendor"]
# default_profile = "sisyphus"
# [devenv.lan]
# interface = "eth0"
# allow = ["192.168.50.10", "192.168.50.0/24"]
# [devenv.profiles.p11]
# branch = "p11"
See zoryn devenv for the full dependency resolution order and per-project overrides.
[flows] and [flows.<name>] — workflows for zoryn flow¶
[flows] default— name of the flowzoryn flowruns when neither aNAMEargument nor[flow] namein.gear/version-upselects one.[flows.<name>] steps— array of inline tables, one per step, executed in order. Each has arunkey (up,build,submit,batch,bash,ssh,devenv,zoryn,agent) plus that type's own keys, and may carryname,if,on-failureandmax-retries. Theup,buildandsubmitsteps take typed option keys (e.g.commit,builder,skip-check) that map to the command's flags; an unset key means the bare command's config-aware default, and each key is also exposed as azoryn flow <NAME>flag. Seezoryn flow› Step options.
Machine config only: read from ~/.zoryn and ~/.config/zoryn/projects.d/<project>.toml (the per-project file wins for a given flow name), never from the package repository — a step carries shell commands and ssh hosts, so a cloned repo must not be able to define one. A repository may only select a flow by name, via [flow] in .gear/version-up.
[flows]
default = "update"
[flows.update]
steps = [
{ run = "up", on-failure = "fix-build", max-retries = 3 },
{ run = "devenv", cmd = "make smoke" },
{ run = "submit", branch = "sisyphus", task-run = true },
]
[flows.fix-build]
steps = [ { run = "agent" } ]
See zoryn flow for every step type, the substituted variables ({pkg}, {old_version}, {new_version}, {tasks}, {task}) and the retry semantics.
[agent]¶
cli— AI CLI used by anagentstep of a flow (default:claude). The value is both the devenv feature enabled for the step and the command run inside the environment. Anagentstep knows the non-interactive form ofclaude,codexandopencode; another value fails the step.flow_steps— set totrueto allowagentsteps to run (default:false). They are off by default because the agent runs in a devenv with your real~/.claude(and equivalents) mounted writable for authentication, so the "edit only the repo" instruction is not sandbox-enforced; only enable it for flows and repositories you trust. Seezoryn flow› AI-agent repair.
~/.config/zoryn/builders.d/¶
Each .conf file defines one builder — a local or remote hasher machine.
# ~/.config/zoryn/builders.d/arm-server.conf
[builder]
name = "arm-server"
type = "remote"
arch = "aarch64"
branch = "sisyphus"
host = "builder-arm.internal"
hasher_dir = "~/hasher"
remote_dir = "~/build"
# hasher_number = 1 # for parallel builds (requires hasher-useradd --number=N)
[commands]
upload = "rsync -av {tarball} {host}:{remote_dir}/"
build = "hsh -v --lazy-cleanup --apt-config=$HOME/hasher_{hasher_number}.env/{branch}/apt.conf {hasher_dir} {remote_dir}/{tarball_name}"
# download uses smart download by default (only newly built packages)
cleanup = "ssh {host} \"rm -rf {remote_dir}/*\""
shell = "hsh-shell {hasher_dir}"
install = "hsh-install {hasher_dir} {packages}"
[builder]¶
name— builder name (used for--builder).type—localorremote.arch— target architecture (x86_64,aarch64,i586, …).branch— target branch (sisyphus,p11, …).host— SSH host for remote builders.hasher_dir— hasher working directory (default:~/hasher).remote_dir— working directory on remote host.hasher_number— hasher subconfig number for parallel builds.repo_workdir— root directory under whichzoryn task mkrepowrites generated task repositories (<repo_workdir>/<task_id>/repo). Overrides the global[builders] repo_workdir.
[commands]¶
upload— upload tarball (runs locally).build— build command (wrapped in SSH for remote builders). Optional: leave it out and zoryn uses the same defaultzoryn builder addwould have written for this builder —hsh … --lazy-cleanup --apt-config=<path>, where the path is{apt_tmpdir}/apt.confwhenrepois set and$HOME/hasher_{hasher_number}.env/{branch}/apt.confotherwise. That second file is written bybuilder add; abuilders.dentry composed by hand needs it to exist, or an explicitbuildhere. Whenever the command contains{apt_tmpdir}, zoryn renders the builder's own apt config into a temporary directory before each build — onemktempplus onersyncper build on a remote builder.zoryn builder configprints the command that will actually run, default or not.download— download results (runs locally). Smart by default: downloads only newly built packages viarsync --files-from. Old configs with full repo download are auto-migrated.list_rpms— list RPMs with mtimes in hasher repo (default:find {hasher_dir}/repo -name '*.rpm' -printf '%P\t%T@\n'). Feeds smart download.cleanup— cleanup command after build.shell— forzoryn builder shell.install— forzoryn builder install.check_busy,download_rpms,upload_rpms— auto-generated if not specified.
To customise --mountpoint=... — for example to drop /dev/kvm on shared hosts without virtualisation — override build and rebuild here. zoryn validates mountpoints against the effective command, not the default. See Builder without /dev/kvm.
Variables available in command templates:
{host}, {hasher_dir}, {remote_dir}, {tarball}, {tarball_name}, {results_download_dir}, {git_root}, {packages}, {batch_repo}, {arch}, {name}, {hasher_number}, {branch}.
See zoryn builder add for the interactive and mass-creation workflows.
[build]¶
timeout— how long (minutes) zoryn waits for a free builder slot before giving up.inactivity_timeout— how long (minutes) a running build may produce no new log output before it is killed as stalled (default: 60). Set to0to disable. The clock starts only once the build writes its first byte, so a long silent pre-output phase (e.g. chroot setup) is never mistaken for a stall — as a consequence, a build that hangs before writing any log output is not caught by this check. Raise the value (or set0) on slow builders where a single huge compilation unit or an LTO link can legitimately stay silent longer than an hour.max_log_mb— maximum build log size in MiB before the build is killed as stalled (default: 1024). Set to0to disable. Catches builds stuck in a tight output loop where the log keeps growing but no real build work is done.
All three [build] keys may also be set once in the global ~/.zoryn [build] section; a value in a builder's own builders.d/*.conf always takes precedence over the global one.
Parallel builds only
The stall watchdog (inactivity_timeout / max_log_mb) runs only in the parallel dispatcher — i.e. when building on 2 or more builders without --sequential, or in the interactive --top TUI (which always uses the dispatcher). A single-builder, non---top --sequential run executes each build in-process with no monitoring, so these keys have no effect there (zoryn prints a warning when they are set but the run is sequential).
Remote builders: local slot only
On a remote builder the kill tears down the local ssh process but does not cross the SSH boundary, so a stalled remote hsh is not directly reaped — it keeps holding the builder's hasher workdir until it finishes on its own. The watchdog frees the local slot immediately, so the next task may be queued to the same remote builder. hasher's workdir lock keeps the two from corrupting each other, but the new build then blocks on that lock producing no log output, and the inactivity watchdog does not start its clock while a log is empty — so that builder can stay wedged until the orphan finishes (a deadlocked orphan wedges it for the rest of the run). On local builders the whole build process group is torn down.
Killed builds leave hasher state
A watchdog (or Ctrl+C) kill sends SIGTERM, waits a short grace, then SIGKILL. A build killed by SIGKILL cannot run hasher's cleanup, so bind mounts and a dirty workdir may be left on the builder and accumulate across a run. If a builder starts failing after repeated stalls, clean its hasher workdir (e.g. hsh --initroot / unmount leftovers) before reusing it.
.gear/version-up¶
Per-package overrides for how zoryn converts upstream tags to RPM versions, where to fetch CVEs from, and merge strategy. TOML format.
[version]
pattern = "{major:+}.{minor:+}.{patch:+}"
template = "{major}.{minor}.{patch}"
strip-prefix = "v"
create-alias = true
filter = "minor=4"
[changelog]
file = "CHANGELOG.md"
# or use remote URL:
# url = "https://curl.se/docs/vuln.json"
# parser = "osv-json"
# cve-format = "extended"
# or use OSV API for projects without changelog files (e.g. Wireshark):
# parser = "osv-api"
# osv-package = "gitlab.com/wireshark/wireshark"
# osv-ecosystem = "GIT"
[merge]
use-theirs = ["meson.build", "configure.ac"]
# scheme = "git-merge" # override auto-detection: "git-merge" or "tarball"
[tarball]
gear-update-opts = "--all"
For HTML release notes (Wireshark example):
[changelog]
parser = "web_regex"
url = "https://www.wireshark.org/docs/relnotes/wireshark-{new_version}.html"
web-regex-pattern = 'wnpa-sec-\S+\s+(?<desc>(?:[^.]|\.\d)+?)\.\s+Issue\s+\d+(?:\s*,\s*Issue\s+\d+)*\s*\.\s+(?<id>CVE-\d{4}-\d+)'
web-regex-stop-at = "Prior Versions"
Validate your config with zoryn check version-up.
[version] — tag → RPM version mapping¶
The pattern/template pair is honoured everywhere upstream tags are turned into versions: zoryn up (both the git-merge tag search and the no-watch-file git-tags fallback of the tarball scheme) and zoryn check version. Without a pattern the generic "latest tag" heuristic may pick a bogus non-release tag (e.g. llama.cpp's 9794052 outranking b10103). On the remote-tag paths a broken [version] config is reported as an error (exit 1 / aborted update) instead of a silent "up to date": a pattern matching no tag, tags yielding no comparable version, or a filter naming a group the pattern never captures. On the git-merge tag search a typo'd pattern remains non-fatal.
Placeholder format: {name:length}
name— capture group name (e.g.major,minor,patch,year,month,day).length— digit count specifier:+or*— one or more digits (regex[0-9]+)N(number) — exactly N digits (regex[0-9]{N})x— one or more hex digits (regex[0-9a-fA-F]+), for git-describe-style commit-hash suffixes (e.g. passt tags2026_07_28.f8df3f1)
Literal characters: . matches a literal dot; -, _ match literal hyphen/underscore; any other character is passed through to the regex, so metacharacters keep their regex meaning. (?:…)? marks an optional section, and an alternation such as (mysql|redis)-{major:+}.{minor:+} works as written — a group you write yourself is made non-capturing, so it cannot shift the placeholder numbering. (?:…) is the only (?…) construct supported: named groups, lookarounds and inline flags are rejected with unsupported group in [version] pattern, and an unbalanced parenthesis with invalid [version] pattern.
Pre-releases: tags carrying a pre-release marker (alpha, beta, rc, pre, dev, snapshot, nightly, -alt) are skipped — the template normally drops the pre-release segment, so selecting 2.0.0-rc1 would announce a 2.0.0 upstream never released. The marker is looked for only in the part of the tag the pattern captures, not the whole tag, so a package whose name contains one (orc, mercurial, libevdev) is unaffected. When every matching tag is a pre-release, nothing is selected and the caller reports that as an ordinary state. Note this applies only when a pattern is configured: without [version] the generic heuristic merely prefers stable tags and will still pick a pre-release when that is all upstream has published.
Template field: uses captured group names without length specifier: {major}, {minor}, … A group may carry a default: {patch:0} emits the captured value, or 0 when the group was absent from the tag.
Pattern examples¶
| Upstream tag | pattern | template | RPM version |
|---|---|---|---|
v1.2.3 | v{major:+}.{minor:+}.{patch:+} | {major}.{minor}.{patch} | 1.2.3 |
release-1.2 | release-{major:+}.{minor:+} | {major}.{minor} | 1.2 |
20240115 | {year:4}{month:2}{day:2} | {year}.{month}.{day} | 2024.01.15 |
2.0.0-rc1 | {major:+}.{minor:+}.{patch:+}-rc{pre:+} | {major}.{minor}.{patch} | 2.0.0 |
camlidl113 | camlidl{major:1}{minor:2} | {major}.{minor} | 1.13 |
RELEASE_8_4_5 | RELEASE_{major:+}_{minor:+}_{patch:+} | {major}.{minor}.{patch} | 8.4.5 |
4.18 / 4.18_02 | {major:+}.{minor:+}(?:_{patch:+})? | {major}.{minor}(?:.{patch})? | 4.18 / 4.18.02 |
v5.8-505 / v5.8.1-506 | v{major:+}.{minor:+}(?:.{patch:+})?-{build:+} | v{major}.{minor}.{patch:0}.{build} | 5.8.0.505 / 5.8.1.506 |
2026_07_28.f8df3f1 | {major:+}_{minor:+}_{patch:+}.{build:x} | {major}.{minor}.{patch} | 2026.07.28 |
Optional groups use standard regex (?:...)?. When the optional part is absent in the tag, the corresponding segment in the template is omitted. A {name:default} default only applies to placeholders outside an optional section — inside (?:...)? the whole section is dropped when the group is absent, so the default never fires.
When upstream sometimes drops a component (e.g. v5.8-505 with no patch, then later v5.8.1-506 with one), omitting the segment instead produces a shorter version where the next component lands in the wrong slot — 5.8.505 would then sort above 5.8.1.506 and block the update. Make the component optional in pattern but give it a default in template ({patch:0}) so the layout stays fixed and versions remain monotonic: 5.8.0.505 < 5.8.1.506.
Filtering tags¶
filter limits which tags are considered. filter = "minor=4" matches only tags where captured minor equals 4. Multiple filters: filter = "major=8, minor=4".
Restricting candidates to an upstream branch¶
upstream-branch = "stable/linux-6.18.y" limits update candidates to tags reachable from that branch, resolved against the configured remotes (the upstream remote first). This is the .gear/version-up form of the kernel-team .gear/upstream-branch file and takes precedence over it — see kernel repos for the full semantics.
[changelog] — CVE scanning source¶
Both CVE-YYYY-NNNNN and CVE:YYYY-NNNNN (ISC format) are normalized to the standard CVE-YYYY-NNNNN. Version headers like Product X.Y.Z (status) released on Date (ISC Kea/BIND) are recognised.
file— local changelog path (CHANGELOG.md,NEWS,ChangeLog).url— URL with security advisories (e.g.https://curl.se/docs/vuln.json).parser— parser type:auto(default) — auto-detect from content.osv-json/json— OSV JSON format (used by curl).markdown/md— standard markdown changelog.html/html-table— HTML table with CVE info.osv-api— query OSV API directly (requiresosv-package).oracle-csaf— Oracle CSAF JSON advisory (for Oracle products like MySQL).mozilla— Mozilla security advisories scraped from the mozilla.org security-advisories website. Use forfirefox,firefox-esr,thunderbird.web_regex— universal HTML parser using a PCRE regex with named groups(?<id>...)and(?<desc>...). For upstreams whose CVEs are published on a HTML release-notes page (e.g. Wireshark) and not present in OSV. Requiresweb-regex-pattern; supports optionalweb-regex-stop-atsubstring marker.
osv-package— package name(s) in the OSV database. Accepts a single name, comma-separated list, or TOML array.gitlab.com/wireshark/wireshark,"stdlib,toolchain",["stdlib", "toolchain"].osv-ecosystem— OSV ecosystem (default:GIT; alsoPyPI,npm,crates.io,Go,Maven).oracle-advisory-product— Oracle product name for filtering (e.g.MySQL Server). Auto-detected from SRPM name if not set.oracle-advisory-max-body-size— max response size for CSAF download in bytes (default: ~4 MB).mozilla-product— Mozilla product name used to match the advisory-index<product> <version>entries (e.g.Firefox,Firefox ESR,Thunderbird). Auto-detected from the SRPM name when omitted (firefox→Firefox,firefox-esr→Firefox ESR,thunderbird→Thunderbird).web-regex-pattern— PCRE forparser = web_regex. Must contain named groups(?<id>CVE-...)and(?<desc>...). Matches with non-CVEidare silently dropped. Validated byzoryn check version-upbefore any HTTP request.web-regex-stop-at— case-sensitive substring marker forparser = web_regex. Content from the first occurrence onward is ignored. Use this to skip "Prior Versions" / older-release sections of cumulative HTML pages. If not found, the parser scans the whole document and emits a warning. Empty value is rejected.cve_format— CVE entry format in the RPM changelog:compact(default) — inline:(Fixes: CVE-..., CVE-...).compact_continuation— continuation lines with 4 CVEs per line:+ (fixes: CVE-..., ...).extended— multi-line with OSV descriptions:- Fixes:/* CVE-...: description.- With
compactand an existing spec, the format is auto-detected from the prior changelog style — both+and*sub-item markers are recognised, and the detected style is preserved.
url takes precedence over file if both are set. For parser = osv-api both url and file are ignored — the OSV API is queried directly. For parser = oracle-csaf, url is optional: the latest quarterly CPU URL is generated automatically if absent, and if that quarter's advisory is not published yet the fetcher falls back to progressively older quarters (up to a year) until it finds one. For parser = mozilla, url and file are ignored — advisories are scraped from the mozilla.org security-advisories website: the index page maps <mozilla-product> <new-version> to an advisory (MFSA) slug, and that advisory page is then parsed for its CVE IDs. For parser = web_regex, both url and web-regex-pattern are required; the URL supports {old_version} and {new_version} placeholders (the same substitution applies to markdown/osv-json/html-table URLs), and the page is HTML-stripped (<script>/<style> bodies removed, tags removed, comments and entities decoded, whitespace collapsed) before the regex runs. CVE descriptions are RPM-macro-escaped (% → %%) before being written into the spec %changelog, so upstream prose containing literal % is safe.
Finding osv-package¶
- Open osv.dev and search for the project (e.g.
wireshark,curl). - Open any vulnerability for that project.
- Look at the Affected packages section — the package name is shown (e.g.
gitlab.com/wireshark/wiresharkfor theGITecosystem). - Copy name and ecosystem into the config.
Or query the API directly:
curl -s -X POST https://api.osv.dev/v1/query \
-d '{"package":{"name":"gitlab.com/wireshark/wireshark","ecosystem":"GIT"},"version":"4.4.3"}' \
| python3 -m json.tool | head -20
| Ecosystem | Package format | Example |
|---|---|---|
GIT (default) | Repository URL path | gitlab.com/wireshark/wireshark |
PyPI | PyPI name | requests |
npm | npm name | express |
crates.io | Crate name | tokio |
Go | Go module path | golang.org/x/net |
Maven | group:artifact | org.apache.logging.log4j:log4j-core |
Full list: https://ossf.github.io/osv-schema/#affectedpackage-field
[merge]¶
scheme— override auto-detection of update scheme:"git-merge"or"tarball". Automatically set by--switch-to-upstream-git.use-theirs— files to take from upstream on conflict (comma/space separated). Useful for version-bearing files likemeson.build,configure.ac.
[tarball]¶
gear-update-opts— extra options forgear-update(e.g.--allto extract every directory from the archive).subdir— subdirectory name inside the tarball to extract (passed asgear-update --subdir=<value>). Supports{version}and{name}placeholders, e.g.subdir = "thunderbird-{version}". Useful for tarballs with multiple top-level entries like./and<name>-<version>/(Mozilla source tarballs). Validated against shell metacharacters and path separators before use.
[add_changelog] — per-package override¶
up_template— changelog entry template forzoryn up. Supports{old_version},{new_version},{cves}. Example:up_template = "- {old_version} -> {new_version} {cves}".
[sandbox] — hook sandbox packages¶
Per-package controls for the hybrid sandbox that runs .gear/up.d/ and .gear/merge-up.d/ hooks. This is a .gear/version-up-only section — it is not read from ~/.zoryn.
packages— extra packages to install into the chroot (on top of the spec'sBuildRequires:).gitis always installed.specbr— boolean, defaulttrue. Whenfalse, the package'sBuildRequires:are skipped: the chroot is initialised bare (hsh --initroot-only, no src.rpm build); the configured packages (git,[sandbox.chroot] packagesfrom~/.zoryn, andpackageshere) are still installed. Withspecbr = falsea custom[sandbox.chroot] preparecommand from~/.zorynis ignored (a warning is printed).
[specsubst] — specsubst values for submit¶
One key per specsubst variable. Currently kflavour is read for kernel-image repos (comma-separated for several tags). Overrides the branch's <flavour>/<dist> prefix; -k on the command line wins over both.
[flow] — which flow to run¶
name— flow thatzoryn flowruns in this package when noNAMEargument is given. Only a selection: the flow itself must be defined in your machine config ([flows.<name>]in~/.zorynor~/.config/zoryn/projects.d/<project>.toml), and an unknown name simply fails the run. A package repository can therefore never contribute steps, only pick among yours.
Date-based versions — example¶
.gear/release-targets and .gear/upstream-branch — kernel repos¶
Kernel gear repositories (the ALT kernel-team convention) carry two extra files that constrain which upstream tags zoryn up may pick during automatic version discovery. zoryn reads them when they exist; repositories without them are unaffected.
.gear/release-targets — one line per ALT branch, <branch> <flavour>...:
The second and following columns are kernel flavours (std-def, un-def, 6.18 — in modern kernel-image-<series> repositories the flavour simply matches the series). zoryn derives the ALT branch from the current git branch name (<flavour>/<dist> such as 6.18/sisyphus, or a bare p11; master counts as sisyphus) and checks it against the file: if the branch is not listed, zoryn up stops with an error — the package is not meant to be updated on that branch. Blank lines and # comments are ignored.
.gear/upstream-branch — the upstream branch tags are taken from, e.g.:
Only the first word of the first non-comment line is the branch name (the rest is an informational repository URL; blank lines and # comments are skipped). Only tags reachable from that branch are update candidates — that is what pins the series: on stable/linux-6.18.y the newest reachable tag is the latest v6.18.x, and newer tags from master are never picked. The value may name the branch with a remote prefix (kernel-team style, stable/linux-6.18.y) or plainly (linux-6.18.y); zoryn resolves it against the configured remotes (the upstream remote first) and stops with an error when nothing matches — silently considering all tags would defeat the series pin (fix the branch name, or bypass with --tag). The upstream-branch key in .gear/version-up sets the same restriction and takes precedence over the file.
The restriction applies only to update candidates; recognising the current version keeps working across a series migration (spec at 6.17.x, target 6.18). An explicit zoryn up --tag TAG bypasses the restriction, including the release-targets branch gate.
Filtering by tag shape — skipping -rcN pre-releases or requiring vX.Y.Z — is not this restriction's job: that is what the [version] pattern in .gear/version-up does, and its pre-release rule skips rc tags on its own.
Two more safeguards accompany the restriction:
- Fresh-tag guard. While an upstream-branch restriction is active, a tag younger than one hour is skipped (upstream mirrors need time to settle) and the update is postponed — zoryn does not fall back to an older candidate.
--forcetakes the fresh tag anyway. - Release base per branch. In repositories carrying the kernel-team files, the
Release:reset after a version bump follows ALT tradition:alt2onc10f2/c9f2certification branches,alt1everywhere else.
Validation¶
zoryn validates config files when it reads them and prints a warning for any unknown section or key — for example unknown key 'pacakges' in [sandbox.hasher]. Warnings are non-fatal; the rest of the config is still applied. This catches typos such as putting packages under [sandbox.hasher] instead of [sandbox.chroot].