Skip to content
zoryn/ maintainer-assistant

Configuration

zoryn reads configuration from three places, in order of specificity:

FilePurpose
~/.zorynGlobal user config (TOML). Required for zoryn to work.
~/.config/zoryn/builders.d/*.confOne .conf per builder — local or remote hasher machine.
.gear/version-upPer-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:

[gitery]
remote = "gitery"

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 environment writes hsh -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 (~/hasher by default). Use hsh, not gear-hsh — the tarball is created separately with gear --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 to hsh --packager= for local hasher builds (default: unset — hsh uses its own configured packager).

[builders]

  • default — default builder(s) when no --builder is 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 (on or off, 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 by download_rpms / upload_rpms.
  • repo_workdir — root directory under which zoryn task mkrepo writes 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 by zoryn gitery to resolve a bare repository name to people/<login>/packages/<name>. Required for bare-name resolution; unused when a full path or -n/--namespace is given. When this key is unset, the login is derived from your git user.email if it is an @altlinux.org address — its local part (before the @) becomes the login.

[gitoskop]

  • url — gitoskop HTTP API base URL, used by the read commands of zoryn gitery and as the base of the built-in gitoskop MCP server of zoryn agent mcp (default: https://git.altlinux.org/gitoskop/api). A trailing slash is stripped.

[gyle]

  • host — SSH alias for the gyle build system.

[sources]

[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 %packager email 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 by zoryn 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 as 2G, 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 for zoryn 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):

[submit]
run = true
test_only = false

[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 as ssh_config(5) ControlPersist.
  • alive_intervalServerAliveInterval for the multiplexed connection, in seconds (default: 15).
  • alive_count_maxServerAliveCountMax for the multiplexed connection (default: 3).
  • Socket files are stored in $XDG_RUNTIME_DIR/zoryn/ (or $TMPDIR fallback) 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" — if git is overridden, git.fetch uses the override.
  • ~/ and $HOME are expanded to absolute paths.
  • Shell metacharacters (;, |, `, $()) are rejected at startup.

[devenv]

  • backend — default backend for zoryn devenv: bwrap or podman. When unset, podman is used if installed, otherwise bwrap.
  • packages — extra packages to install in the development environment on top of the spec's BuildRequires: (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 IMAGE overrides it per invocation. Not read from .gear/devenv.
  • build — array of shell commands run as RUN steps 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 — like build, but run as the host user (per-user installers, e.g. curl … | bash into ~/.local/bin); use sudo for root steps. Machine config only.
  • mounts — array of host paths / host:container[:opts] specs bind-mounted into the podman container (each -v at container-create time, in addition to the always-mounted project directory). A bare path is bound at the same path. An entry written as tmpfs:<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 needs mode=1777 among the options — there is nothing left to chown it afterwards. A spec may reference {devenv} (the environment's container name, as in zoryn devenv list; on bwrap, which has no container, the devenv-<project> hostname), {project} (the project directory's basename), {branch} (the effective ALT branch) and {profile} (the active profile, default when 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, 0 or a negative value for unlimited). The same value is also applied as --ulimit nproc=<n>:<n>, so ulimit -u inside the container matches the cgroup cap instead of podman's much lower default (~512) — raise this key when a parallel build fails with posix_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 --init so zombie subprocesses (e.g. git's background auto-gc) are reaped. Podman backend only; machine config only.
  • prompt — literal bash PS1 string 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 host onto a rootless pasta network 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 (like mounts, 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.conf fields, passed to podman as --dns (nameserver addresses, IPv4/IPv6 or the literal none), --dns-search (search domains, or . to empty the list) and --dns-option (resolver options such as ndots:2). Each is an array and applies to both the image build and the container, so apt resolves names at build time too. Set them when the container cannot reach the host's resolvers — typically alongside outbound_interface, whose pasta binding routes DNS out that interface. zoryn devenv --dns ADDR / --dns-search DOMAIN / --dns-option OPT set them for one run. Layer-select per key (like apt_sources, unlike mounts): flag > per-project projects.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 — when true, forward the host SSH agent into the podman container (host $SSH_AUTH_SOCK bind-mounted to /run/ssh-agent.sock) so tools inside use your agent keys; zoryn devenv --ssh-agent/-A enables 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 port 18080 to container port 8080, so a dev server inside is reachable at http://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 is 127.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 when outbound_interface is unset — with --network host the container already shares the host's ports. Changing the list recreates the container. Podman backend only; machine config only (~/.zoryn and ~/.config/zoryn/projects.d/<project>.toml, never .gear/devenv).
  • timezone — IANA zone name (e.g. "Asia/Almaty") passed to podman run --tz, which sets TZ and /etc/localtime inside the container. When unset, the container keeps the image default (UTC). zoryn devenv --timezone overrides it for one run; changing the effective zone recreates the container. Podman backend only — bwrap inherits the host TZ; machine config only — not read from .gear/devenv.
  • env — array of NAME=value environment entries set in the environment's shell, e.g. env = ["GOFLAGS=-mod=vendor", "EDITOR=vim"]. With podman they are passed as --env at container creation, so the enter shell (and zoryn devenv install) inherits them; changing the list recreates the container (image layers are reused). With bwrap they become --setenv at enter time. Like ports, the key concatenates across layers: global ~/.zoryn first, 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-formed NAME=value pair with no control characters, no trailing space, and no braces beyond the {devenv}/{project}/{branch}/{profile} placeholders, which expand in env values exactly as in mountsenv = ["HERDR_SESSION=~/.config/herdr/sessions/{devenv}"] tells a tool inside the container its own per-environment directory. With forward_ssh_agent, the forwarded SSH_AUTH_SOCK wins over an env entry of that name. The entries reach only the environment's shell — the podman image build (build/build_user steps, feature installers, apt) does not see them, so a build-time proxy belongs in build steps or in the apt configuration, not here. Machine config only — never read from .gear/devenv, so a package repository cannot plant LD_PRELOAD & co. into your shell.
  • branch — target ALT branch (default sisyphus); also derives the default podman base image when image is unset. zoryn devenv --branch/-B overrides it for one run.
  • apt_config — host apt config path: an apt.conf-format file, a sources.list file, or a directory containing either. For the bwrap backend: normalised to an apt.conf file for hsh --apt-config (a sources.list is 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's sources.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's sources.list (e.g. "local"). Podman backend only; machine config only.
  • apt_sources — array of apt source lines written verbatim into the podman container's sources.list (e.g. ["rpm file:///mnt/alt x86_64 classic"]). Local file: 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 --profile is 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) and allow (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 /32 host route; host names are not accepted — DNS keeps leaving through the tunnel by design. Requires outbound_interface to be set and to differ from interface. 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). Editing allow does not recreate the container — routes are reconciled into the running one on the next zoryn devenv; zoryn devenv inspect shows 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 flow zoryn flow runs when neither a NAME argument nor [flow] name in .gear/version-up selects one.
  • [flows.<name>] steps — array of inline tables, one per step, executed in order. Each has a run key (up, build, submit, batch, bash, ssh, devenv, zoryn, agent) plus that type's own keys, and may carry name, if, on-failure and max-retries. The up, build and submit steps 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 a zoryn flow <NAME> flag. See zoryn 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 an agent step of a flow (default: claude). The value is both the devenv feature enabled for the step and the command run inside the environment. An agent step knows the non-interactive form of claude, codex and opencode; another value fails the step.
  • flow_steps — set to true to allow agent steps 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. See zoryn 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).
  • typelocal or remote.
  • 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 which zoryn task mkrepo writes 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 default zoryn builder add would have written for this builder — hsh … --lazy-cleanup --apt-config=<path>, where the path is {apt_tmpdir}/apt.conf when repo is set and $HOME/hasher_{hasher_number}.env/{branch}/apt.conf otherwise. That second file is written by builder add; a builders.d entry composed by hand needs it to exist, or an explicit build here. Whenever the command contains {apt_tmpdir}, zoryn renders the builder's own apt config into a temporary directory before each build — one mktemp plus one rsync per build on a remote builder. zoryn builder config prints the command that will actually run, default or not.
  • download — download results (runs locally). Smart by default: downloads only newly built packages via rsync --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 — for zoryn builder shell.
  • install — for zoryn 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 to 0 to 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 set 0) 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 to 0 to 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 tags 2026_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 tagpatterntemplateRPM version
v1.2.3v{major:+}.{minor:+}.{patch:+}{major}.{minor}.{patch}1.2.3
release-1.2release-{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
camlidl113camlidl{major:1}{minor:2}{major}.{minor}1.13
RELEASE_8_4_5RELEASE_{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-506v{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 (requires osv-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 for firefox, 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. Requires web-regex-pattern; supports optional web-regex-stop-at substring 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; also PyPI, 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 (firefoxFirefox, firefox-esrFirefox ESR, thunderbirdThunderbird).
  • web-regex-pattern — PCRE for parser = web_regex. Must contain named groups (?<id>CVE-...) and (?<desc>...). Matches with non-CVE id are silently dropped. Validated by zoryn check version-up before any HTTP request.
  • web-regex-stop-at — case-sensitive substring marker for parser = 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 compact and 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

  1. Open osv.dev and search for the project (e.g. wireshark, curl).
  2. Open any vulnerability for that project.
  3. Look at the Affected packages section — the package name is shown (e.g. gitlab.com/wireshark/wireshark for the GIT ecosystem).
  4. 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
EcosystemPackage formatExample
GIT (default)Repository URL pathgitlab.com/wireshark/wireshark
PyPIPyPI namerequests
npmnpm nameexpress
crates.ioCrate nametokio
GoGo module pathgolang.org/x/net
Mavengroup:artifactorg.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 like meson.build, configure.ac.

[tarball]

  • gear-update-opts — extra options for gear-update (e.g. --all to extract every directory from the archive).
  • subdir — subdirectory name inside the tarball to extract (passed as gear-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 for zoryn 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's BuildRequires:). git is always installed.
  • specbr — boolean, default true. When false, the package's BuildRequires: are skipped: the chroot is initialised bare (hsh --initroot-only, no src.rpm build); the configured packages (git, [sandbox.chroot] packages from ~/.zoryn, and packages here) are still installed. With specbr = false a custom [sandbox.chroot] prepare command from ~/.zoryn is ignored (a warning is printed).
[sandbox]
specbr = false
packages = ["go", "make", "curl"]

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

[specsubst]
kflavour = "for-vm"

[flow] — which flow to run

  • name — flow that zoryn flow runs in this package when no NAME argument is given. Only a selection: the flow itself must be defined in your machine config ([flows.<name>] in ~/.zoryn or ~/.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.
[flow]
name = "kernel-update"

Date-based versions — example

[version]
pattern = "{year:4}{month:2}{day:2}"
template = "{year}.{month}.{day}"

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

sisyphus 6.18
p11 6.18

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

stable/linux-6.18.y

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. --force takes 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: alt2 on c10f2/c9f2 certification branches, alt1 everywhere 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].

  • Hooks.gear/merge-up.d/, .gear/up.d/, syntax highlighting, log themes
  • Sandboxhybrid / bwrap / direct runners for hook isolation