Перейти к содержанию
zoryn/ maintainer-assistant

Changelog

Unreleased

Added

  • zoryn build and the build stage of zoryn up: inside a devenv without the hasher feature the build stops up front with the gear-rpm -ba --commit command instead of heading for the host's builders, which are visible through the mounted config but unreachable from the container
  • zoryn devenv: the bundled Agent Skill directory is bind-mounted read-only at its host path, so the pointer skill installed by zoryn agent skill install resolves inside the container; the mount is part of the container key, so existing containers are recreated once
  • Agent Skill: references/repair.md — the unattended build-repair workflow (what to read, what may change, verifying with gear-rpm, when to stop); the zoryn flow agent step's built-in prompt now points at it instead of carrying its own instructions

Changed

  • Agent Skill: building inside a devenv with gear-rpm, not zoryn build/hasher, is now a rule at the top of SKILL.md and in references/devenv.md, with the reason (BuildRequires installed, host builders out of reach), instead of a soft preference at the end; the pointer skill's fallback says the same in two lines

  • zoryn devenv: the claude and GLM-claude-code features take an installer option — native (default, the official install.sh) or npm (@anthropic-ai/claude-code from the npm registry, for networks where the native download is slow)

  • zoryn devenv --outbound-interface IFACE / -I: one-shot override of [devenv].outbound_interface; -I '' unsets it

Fixed

  • zoryn gen version-up: a tag with both a prefix and a suffix (v1.5.0-stable for version 1.5.0) produced a pattern that matched nothing, so zoryn up then failed with "Cannot find any tags for current version". The suffix is now embedded in the pattern as a literal (v{major:+}.{minor:+}.{patch:+}-stable) with a template that drops it, the same way a prefix already was; the strip-suffix key, which the generator wrote but nothing ever read, is no longer emitted (--update removes a stale one). Closes #145
  • zoryn devenv (podman): changing [devenv].outbound_interface (or --outbound-interface) no longer rebuilds the image. The interface only selects the network the build runs over, not what it installs, yet it was part of the image tag, so flipping it paid a full podman build for an identical Containerfile. It now stays in the container key only (like mounts): the container is recreated from the cached image
  • parallel builds: aborting the remaining builders (should_abort, Ctrl+C) could hang — a kill racing a just-forked child was lost because its process group did not exist yet; it now falls back to the child's pid
  • task manage: the deps screen now lists a dependency whose task cannot be fetched (typically deleted with task rm) as a dim placeholder row instead of dropping it, so d can remove it; previously the screen showed only "fetch failed" with no way to unblock the task
  • task test-rebuild: a no-op run now saves an empty, completed rebuild-stats.json snapshot before exiting

0.52.0 - 2026-09-01

Fixed

  • zoryn devenv: shell completion for --feature/-F and zoryn devenv feature add now lists local features from ~/.config/zoryn/devenv/features/ (both the flat <id>.toml and the <id>/feature.toml layout), not only the bundled ones
  • zoryn devenv: SHELL=/bin/bash is now exported into the environment (podman --env at create, bwrap --setenv at enter) — bash self-assigns $SHELL from passwd but does not export it, so tools reading the environment (terminal multiplexers, herdr) saw no SHELL and fell back to /bin/sh; a configured [devenv] env entry can still override it. Existing podman containers pick the fix up on the next recreation (--rebuild, or any config change that recreates)

Added

  • zoryn devenv (podman): the last resolved BuildRequires set is memoised per project+branch+arch (~/.cache/zoryn/devenv/buildreqs/) and baked into the derived image as a best-effort warm-up layer, so a container recreated for a reason outside the cache key (an env/ports/dns/timezone change, a new SSH agent socket, --rebuild) starts with its build dependencies already present and the in-container resolution installs only the delta — instead of re-downloading every build dependency. The layer is || true and sits last, so a stale memo can never fail the build and a memo-less image shares every earlier layer; the memo digest is folded into the image tag, never the container name
  • zoryn devenv: a local feature definition can now be a flat ~/.config/zoryn/devenv/features/<id>.toml file, alongside the existing <id>/feature.toml directory form (which wins when both exist); the unknown-feature error names both expected paths
  • zoryn devenv: {devenv}, {project}, {branch} and {profile} placeholders in mount specs — config mounts, --mount-bind and feature mounts alike — so one entry gives every environment its own host directory (mounts = ["~/.config/herdr/sessions/{devenv}"]); the same placeholders expand in [devenv] env and feature env values, so a tool inside the container can learn its per-environment directory. {devenv} is the container name from devenv list (bwrap substitutes the devenv-<project> hostname), expanded late so the cache key keeps the literal token; the rest expand before the existence check and the key; {profile} reads default when no profile is active, unknown {tokens} stay as written
  • zoryn devenv: [devenv] env config key — an array of NAME=value entries set in the environment's shell (env = ["GOFLAGS=-mod=vendor"]). podman passes them as --env at container creation (the enter shell inherits them; a changed list recreates the container in place via the recorded zoryn.env label), bwrap as --setenv at enter time. The key concatenates global ~/.zoryn and projects.d layers, later entries winning per name; machine config only, never .gear/devenv. Features may declare env in feature.toml too (validated at parse, ${NAME} option substitution as in mounts), applied before the user's config
  • zoryn flow: the agent step takes typed option keys — cli overrides [agent] cli for one step, and prompt (templated like a cmd) replaces the built-in repair prompt, so a flow can point the agent at any job, not only fixing the newest build log; a step with its own prompt no longer requires a build log to exist. Like every typed key, both are also implicit zoryn flow <NAME> flags (--prompt '...', --cli codex). The [agent] flow_steps = true gate still applies

0.51.0 - 2026-08-29

Fixed

  • zoryn up / zoryn check version: an upstream tag whose commits were already fetched no longer hides a new release behind "up to date" — git auto-follows tags only for objects downloaded in the same fetch, so such a tag never arrived and the upstream-branch filter dropped the new version as "not merged". The staleness probe now also compares the remote's tag list and the sync fetch grabs tags explicitly (git fetch --tags)

  • zoryn up: hooks from .gear/up.d/ ran on every invocation, including an "already up to date" run that found no newer version — re-firing hooks that already ran when the package was actually updated. The stage is now skipped unless this run picked up a new version, mirroring the .gear/merge-up.d/ gate; --stage up-hooks / --from up-hooks remain the explicit way to run the hooks on demand

  • zoryn devenv (podman): clients connecting to the container from an address inside a [devenv.lan] allow prefix were silently blackholed — the sidecar's destination route captured the replies of connections that had entered through the primary interface, and the pasta sidecar, a socket-level relay, drops mid-stream packets of flows it did not initiate; the client's connection would hang on the first data segment and die with a RST after ~19 s, while outbound connections to the same subnet worked fine (with allow = ["10.0.0.0/8"] this broke, for example, every office-VPN visitor of a service published from the devenv). The sidecar's routes now live in a dedicated routing table entered through a pair of policy rules that split traffic by source: fresh outbound connections enter the table, replies sourced from the container's primary address bypass it. Running containers migrate on the next zoryn devenv — the reconcile moves the routes and removes the stale main-table ones, no recreate needed. Two consequences of the source-based split are worth knowing: a UDP service bound to the wildcard address still replies over the LAN path (its replies have no source at routing time), and an allow-listed prefix now wins over the container's own connected subnet, which the main-table route used to keep for itself

Added

  • zoryn devenv (podman): entering an environment now warns when a bind-mounted file was replaced on the host since the container was created — a file mount is pinned to its inode, and a host tool rewriting the file via tmp+rename (how most configs are saved, ~/.claude.json included) silently leaves the container on the old version. The inodes of file mounts are recorded in a zoryn.mount_inodes container label at creation and compared on every enter; a mismatch is reported right before the shell opens, recommending zoryn devenv --rebuild
  • zoryn agent mcp: built-in bugzilla server — the ALT Linux Bugzilla MCP endpoint (https://bugzilla.altlinux.org/mcp.cgi) — and a new install --api-key KEY option that stores the key as an HTTP header in the agent config (header name from the catalog's api-key-header; bugzilla's is X-Bugzilla-Api-Key). Without a key the server answers anonymously; reinstalling without --api-key keeps a previously configured key, and an empty --api-key '' removes it. Local ~/.config/zoryn/mcp/<name>.toml definitions may set api-key-header too
  • zoryn flow: first-class task-run step — { run = "task-run", commit = true } starts the created task's build via zoryn task run, with typed keys task (default {task}), commit and message
  • zoryn flow: first-class task-approve step — { run = "task-approve", message = "sign" } runs zoryn task approve on the created task; subtask defaults to all, message becomes the -m comment
  • zoryn flow: first-class task-add step — { run = "task-add", args = ["rebuild"], dependent-on = "kernel-image-for-vm" } runs zoryn task add with typed, CLI-overridable keys (task, dependent-on); task defaults to {task}, the task the submit step created, and all values are templated
  • zoryn flow: -B is accepted as the short form of the implicit --branch step flag, matching the rest of zoryn; bash completion now offers the selected flow's step flags and completes branch names after -B/--branch (zsh already completed branches). An ambiguous flag now lists the concrete qualified forms (--up.branch or --submit.branch) and suggests distinct name keys when steps of the same type collide
  • zoryn devenv packages: print the packages a zoryn devenv run would install, without preparing an environment — the sorted unique union of the spec's static BuildRequires (no rpmbuild expansion), configured [devenv] packages (~/.zoryn, .gear/devenv, projects.d), and packages from the selected features. --verbose groups the list by source; --profile / --backend / --feature / --no-build-deps have the same meaning as on zoryn devenv
  • zoryn up --no-build: run the update (version bump, spec, tags, hooks) without building — useful when a later step must run before the build
  • zoryn up --switch-to-upstream-git --force: re-run the migration on a repository that already uses the upstream git scheme — the re-merge overlays the upstream tree again, repairing a previously botched merge; without --force the command still refuses as before
  • zoryn task run/add/approve (and disapprove) accept a comma-separated list of task ids (123456,123457), applying the operation to each in order and stopping at the first failure — mirrors submit -B
  • [sandbox] outbound_interface (~/.zoryn or projects.d/<project>.toml): route all hook traffic through one host interface, by the same pasta mechanism as devenv's outbound_interface — the hook's bwrap sandbox runs inside a pasta --config-net network namespace whose outbound sockets are bound to the interface. bwrap/hybrid runners only; requires the passt package (a missing pasta fails the hook rather than silently keeping the host network)
  • zoryn flow: unattended package-update workflows with custom bash/ssh/devenv steps, on-failure recovery flows and AI-agent repair (experimental). A flow is a [flows.<name>] list of steps in the machine config (~/.zoryn or ~/.config/zoryn/projects.d/<project>.toml — never the package repo, which may only select one via [flow] name in .gear/version-up), each step being up, build, submit, batch, bash, ssh, devenv, zoryn or agent, optionally guarded by an if condition and repaired by an on-failure flow up to max-retries times. AI-agent repair is opt-in — an agent step runs only when [agent] flow_steps = true, since it mounts your real credentials writable. The up, build and submit steps take typed option keys (e.g. commit, builder, skip-check, parallel) mapped to the command's flags, and each is also an implicit-by-name flag on zoryn flow <NAME> (--commit, --builder X, --no-commit, or --<step>.<key> when two steps share a key) that overrides the step's config value for one run. An unset option means the bare command's config-aware default, so a flow build now honours [builders] parallel. The up step also takes no-build, which updates the package without building it so a later step can build (a kernel flow can bump the version, generate a signing key, then build). {pkg}, {old_version}, {new_version}, {tasks} and {task} are substituted into commands - {tasks} is every created task id comma-joined so a multi-repo submit (branch = "sisyphus,p11") can drive zoryn task run/add/approve over all of them, and {task} stays the first id for single-repo configs; an up step that finds no new version ends the run successfully, a failed step saves its position so zoryn flow --continue retries exactly that step, and zoryn flow list/show/--dry-run inspect what would run. Nothing prompts, so a nightly update fits in one cron line

0.50.0 - 2026-08-22

Fixed

  • zoryn devenv (podman): a config-layer apt source (apt_config/apt_builder/apt_sources) whose repositories name a different ALT branch than the effective devenv branch is now dropped with a warning, keeping the base image's own apt sources — a global apt_config pointing at a sisyphus mirror used to replace the p11 image's sources on -B p11 and break the environment. An explicit --apt-conf value is still applied verbatim, and the new --apt-conf '' form is a one-shot reset that ignores any configured apt source

  • zoryn task batch refused the config task genbatch had just written: the base member of a gear specsubst group carries an empty value (specsubst = "subpackage="), which the config validator rejected as "has an empty value". An empty specsubst value is legitimate — the base package substitutes the empty string — and the whole pipeline below (git config, gear) already handled it; the validator now accepts it

  • per-host build limits in [hosts."<hostname>"] of ~/.zoryn were never applied: the lookup addressed the section with literal quotes in the key, so it silently fell back to the global [hosts] values. Host-specific min_free_ram/max_load_avg/max_io_wait now win, including for hostnames containing dots

  • zoryn task copy now records the created (or --into) task as the last one worked with, so a following zoryn task run without an ID starts that copy instead of the previous remembered task. --dry-run still leaves the remembered task unchanged.

  • zoryn up --switch-to-upstream-git: the file-move commit step failed with fatal: pathspec '.gear-rules' did not match any files whenever the repository kept its rules in .gear-rulesgit mv had already staged the rename, so re-adding the vanished path made git add bail out. Step 5 now stages .gear/ only
  • git operations on tags whose name starts with a dash: git merge, git tag -l and git subtree pull received the bare tag name and parsed it as an option, failing with "unknown switch" (or worse, acting on the option). Tags are now passed after an explicit -- separator or as refs/tags/<name>, matching the call sites that already did this, and validate_tag_name rejects a leading dash and the ^ { } rev-syntax characters that never belong in a tag name
  • zoryn devenv (podman): the generated Containerfile now gives every feature its own group of image layers — the feature's packages in its own apt layer (with its own apt-get update), then its run/run_user steps — after a small project-independent base layer (bash, git, rpm-build), and only then the project-specific tail (the project's own packages, your build/build_user steps, the prompt). Previously the very first apt layer baked the per-project package union, so any difference between projects — or any change to the feature list — invalidated podman's layer cache from the first instruction on and every feature installer (e.g. claude's curl … | bash) re-ran. Now a feature's layers depend only on the features before it: projects sharing a feature prefix share those layers byte-for-byte, and enabling one more feature no longer reinstalls the agents already baked in. The extra apt-get update per feature is cheap next to re-running installers. Cache keys are unchanged, so existing containers and images stay valid; the new layout applies from the next image build. Note the execution-order change it implies: a feature step does not see the project's packages, your build steps, or the packages of features after it — a custom feature must declare everything it needs in its own packages, as the bundled features already do.
  • watch files: @ANY_VERSION@ (and @SEMANTIC_VERSION@/@STABLE_VERSION@) now expand with the version in a capturing group, as uscan defines them. The href-matching path used the expanded pattern as-is, so a watch file relying on these macros matched the tarball links but extracted no version from them — zoryn check version on opari2 reported "No versions found upstream" while the upstream page plainly listed 2.0.10. Patterns with an explicit (...) group were unaffected.
  • zoryn check version / zoryn check upstream: -d now also enables the watch-file debug trace (hrefs found, compiled patterns, matches), which previously only zoryn up -d showed — the failing check above was undiagnosable from its own debug output.
  • zoryn check version: the git-tags check now resolves the upstream URL the same way zoryn up does — .gear/upstream/remotes, then the spec Vcs: tag, then URL:, each candidate verified with git ls-remote — instead of accepting only a fixed list of forges (GitHub, GitLab, Codeberg, sr.ht). A kernel-style spec with VCS: https://git.kernel.org/... reported "No watch file or git URL available" while zoryn up updated the same package fine; now both commands agree on the upstream source. The git ls-remote probe is a network call, so it runs only when the check actually falls through to git tags, not while a watch file answers.
  • zoryn check version: without a .gear/version-up pattern, the git-tags check now anchors on the tag carrying the current version and considers only tags in the same naming style, like zoryn up does — a repo whose history carries foreign-style tags no longer gets them as the "newest version" (c-ares, which inherited curl's tags, was offered 1.34.8 -> 7.19.4 from curl-7_19_4 while up correctly said up to date). The style-blind heuristic remains only as a fallback when no tag carries the current version.
  • zoryn check version: the git-tags check now also honours the release-targets/upstream-branch restriction zoryn up applies (.gear/release-targets, .gear/upstream-branch, upstream-branch in [version] of .gear/version-up) — candidate tags are limited to those merged into the locally fetched upstream branch. Without it a kernel repo pinned to the 6.18 series was offered 7.x while up correctly stayed on 6.18. Like in up, a configured branch that cannot be resolved fails the check rather than silently widening the candidate set.

  • zoryn up / zoryn check version: a stale local upstream branch no longer hides new versions. The release-targets/upstream-branch filter answers from the locally fetched branch while check's candidates come fresh from ls-remote, so on a kernel repo that had not been fetched since the last release the check saw the new tag upstream, dropped it as "not merged into upstream/linux-6.18.y", and reported "Package is up to date". The filter now compares the local tracking ref with the remote's branch head (one git ls-remote of that single ref, best-effort — an unreachable remote changes nothing) and, when the branch is behind, fetches just that branch (git fetch <remote> <branch> — the incremental delta plus the tags pointing into it, seconds on a kernel repo where a full fetch drags in every series branch and the mainline tags) and answers from the fresh state — the check reports the new version right away instead of asking the user to fetch. A warning ("newer versions may be hidden — run 'git fetch '") remains only when the fetch fails or still leaves the branch out of sync. zoryn up fetches the upstream remote itself before the filter runs, so there the probe finds the branch already in sync and changes nothing

Added

  • zoryn submit in kernel-image mode now accepts multiple repositories (-B sisyphus,p11): the flavour tags are created and pushed once, and each branch gets its own new task carrying all flavour subtasks, chained with the usual inter-task dependencies — replacing the manual "copy the sisyphus task into the branch" step. The restriction dated from before the multi-tag machinery could carry more than the first flavour to later repositories; kernel-module submits stay single-repo (their tags are bound to the branch dist)

  • subtask TAB completion (task approve, task delete, task copy --subtask, task add ^/) now asks the task API with brief=2, which answers from its in-memory index without touching disk — completing a subtask no longer stalls on tasks whose full state is expensive to read

  • zoryn task add: the ^SUBTASK / /SUBTASK positional suffixes now also accept the full pkg.git=tag label printed by task show, resolved to the subtask number via the task API (a bare package name stays rejected — several subtasks of one task can share the git repo, only the tag is unambiguous). Tab completion after ^ and / offers both the numbers and the labels, so 429379^zo<TAB> expands to 429379^zoryn.git=<tag> instead of silently matching nothing

  • zoryn gen version-up: new -u/--update flag for regenerating an existing .gear/version-up without losing manual settings — the generated [version] keys (pattern, template, strip-suffix, create-alias, and filter when --filter is given) are replaced in place, while other sections such as [merge] and manually added keys such as upstream-branch are kept untouched. Previously the only option was --force, which overwrites the whole file; the "already exists" error now suggests both

  • zoryn up / zoryn check version: the release-targets/upstream-branch filter no longer pays the full git tag -l --merged history walk (~12 s and ~1 GB on a kernel repo) on every run. The listing is cached in the git dir (zoryn-merged-tags-cache), keyed by the upstream branch tip and the local tag refs, so it is recomputed only when a fetch actually moves the branch or brings a tag; every other run answers from the cache instantly. When the listing does run and takes over two seconds, a warning still recommends the one-time git commit-graph write --reachable && git config fetch.writeCommitGraph true that softens the walk.

  • zoryn devenv: new --no-build-deps flag skipping BuildRequires resolution entirely, so the environment holds only the configured [devenv] packages. Needed for specs whose ExclusiveArch:/ExcludeArch: shuts out the host architecture (e.g. u-boot-* edited on x86_64 and built on an aarch64 builder): rpmbuild -bE refuses such a spec, so the environment could not be prepared at all. When resolution fails on that rpmbuild error, both backends now print a hint recommending the flag.

  • zoryn agent mcp — new subcommand group registering HTTP MCP servers in local AI coding agents (Claude Code ~/.claude.json, opencode ~/.config/opencode/opencode.json, Kimi Code ~/.kimi-code/mcp.json). install NAME... writes servers from the catalog — built into zoryn (currently gitoskop, the read-only ALT gitery mirror, URL derived from [gitoskop] url) or defined by the user in ~/.config/zoryn/mcp/<name>.toml (a local definition overrides a built-in of the same name) — or a one-off server via --url; uninstall NAME... removes them; list shows what each agent has configured plus the available catalog. Only the named entry is touched, so the rest of each agent's config survives intact: a config that cannot be read or parsed is reported and left alone rather than rewritten from scratch, a symlinked config is followed instead of replaced, an existing file keeps its permissions and a new one is created 0600 (these files carry credentials), and a target that cannot be renamed over — the bind-mounted config inside a devenv — is rewritten in place. Agents are handled independently: one failing config does not stop the others. Registration is per user, so devenv environments that bind-mount the agent configs pick it up automatically.

0.49.0 - 2026-08-14

Added

  • zoryn devenv: new bundled feature pi — installs the pi.dev coding agent into the dev user's ~/.local/bin and bind-mounts the host's ~/.pi/agent and shared ~/.agents directories read-write, sharing provider logins, settings, sessions, extensions, skills and packages with the environment while persisting changes made inside it.
  • zoryn devenv list: the listing now has a header and two more columns — the features an environment was built with and the profile it came from (- when it has neither). The container name carries only a cache key, which says nothing about what went into it, so two environments of the same project were indistinguishable in the list; With --verbose the listing also names, for every project with more than one environment, the labels whose value is not the same in all of them — usually the computed packages or the spec digest, which is what environments with the same features part over. zoryn devenv inspect NAME still prints the whole recorded configuration of one.
  • zoryn devenv: new bundled feature hasher — builds packages with hasher inside the environment. hasher-priv refuses to work where it cannot see a real host, so the feature installs hasher-kayfabe alongside it (0.2.1 or newer; an older one stops the image build with a message rather than half-activating), creates the satellite users for the dev user, and puts wrappers in front of hsh and friends that start hasher-privd on first use — an environment that never builds anything never runs a daemon. It also permits /proc, /dev/pts and /dev/kvm in /etc/hasher-priv/system: hasher-priv refuses any mountpoint not listed there, and zoryn's own build commands pass --mountpoints=/proc,/dev/pts. The environment gets CAP_SYS_ADMIN (hasher-priv mounts), which weakens isolation, so select it deliberately; /proc stays masked as podman left it. Root for starting the daemon comes from hasher-kayfabe's setuid helper, restricted to the hashman group, so the sudo feature is not needed — though that group membership is itself a path to in-container root. The hasher workdir is a tmpfs by default ([devenv.features.hasher] tmpfs, set it to "" to keep it on the container's overlay), and the host's ~/.hasher/config is bind-mounted read-only (hasher_config, "" disables) so builds inside use your own hasher settings — the packager above all, which hasher reads from that file and never from ~/.rpmmacros; without it every build dies in sisyphus_check with "wrong PACKAGER". The host's ~/.rpmmacros comes in the same way (rpmmacros) for builds run in the environment directly — gear-rpm and rpmbuild read %packager from there, hasher never does. A feature's read-only mount whose host file does not exist is now skipped with a warning naming it, instead of podman creating a directory at that path on the host and binding that in. Known limit: the container's cgroupfs stays read-only, so builds run without hasher's per-job cgroup limits and print one warning each.
  • zoryn devenv (podman): a [devenv] mounts entry (or a feature's) written as tmpfs:<dest>[:<options>] now becomes --tmpfs <dest>[:<options>] instead of a bind mount, and an entry that expands to nothing is dropped rather than passed to podman as an empty -v — which is how a feature whose mount is an option (mounts = ["${TMPFS}"]) is switched off from the config.
  • zoryn devenv: root access inside an environment is now opt-in through the new bundled feature sudo, and images no longer carry a passwordless sudoers rule for every dev user. The rule was dead weight anyway: ALT ships /usr/bin/sudo as mode 4710 root:wheel (control sudo wheelonly), so a user outside the wheel group could not execute the binary at all and every sudo in a devenv failed with Permission denied. The feature writes the /etc/sudoers.d/<user> drop-in and sets control sudo public, leaving sudoers in charge of what may be run. The sudo package itself is no longer installed into every image either, so environments rebuild once on upgrade; add -F sudo (or [devenv.features.sudo]) where root is needed.
  • zoryn devenv: new bundled feature chromium — installs Chromium with DejaVu fonts (fontconfig included) so headless browsing and screenshots, typically driven by AI agents, work inside podman devenvs. The base alt:latest image lacks Chromium's runtime libraries (NSS, the X11 stack, libgbm, at-spi, libasound, …) and any fonts, so both the distro package and agent-downloaded browsers (puppeteer/playwright) failed to start; the feature also appends --disable-setuid-sandbox to /etc/chromium/default, because the setuid chrome-sandbox helper cannot work under rootless podman while Chromium's namespace sandbox still can — page isolation is kept.
  • zoryn devenv: [devenv] ports entries may now name the host interface or address the port is published on — "eth0:8080", "192.0.2.10:18080:8080", or "*:3000" for every interface — so a server in the container can be reached from the LAN and not only from the host. Without a prefix the bind stays 127.0.0.1, as before. An interface name is resolved to its first IPv4 address when the container is created, so a re-addressed interface needs no config edit; an interface without an IPv4 address, or an address the host does not carry, is reported before the cached environment is touched. A host port is arbitrated per bind address with overlap semantics — the wildcard conflicts with every address, so the same port may be published on several specific addresses, and changing an entry's bind still recognises the running container's old bind as the one being replaced. Overlapping entries ("8080" with "*:8080", or two entries resolving to one address with different guests) are refused up front, and equivalent spellings of one mapping are deduplicated. When a configured interface has no IPv4 address, entering an already-created environment degrades to a warning that keeps its recorded ports — provided those are still bindable: a recorded address that vanished with the interface is reported clearly (pasta could not bind it at start anyway). Creation always fails without the address. localhost is accepted as a bind synonym for 127.0.0.1.
  • [version] pattern in .gear/version-up: new x length specifier matching one or more hex digits ([0-9a-fA-F]+), so git-describe-style tags with a commit-hash suffix are captured — e.g. passt tags 2026_07_28.f8df3f1 match {major:+}_{minor:+}_{patch:+}.{build:x}. Previously only all-digit components could be captured, so such tags were invisible to zoryn up.
  • zoryn task add: new --dependent-on PKG option for the rebuild action — computes the rebuild set from the repository dependency graph instead of naming packages. zoryn takes the source packages whose binaries runtime-depend on PKG (RDB what_depends_src, dptype=binary) and keeps only those pinned to PKG at its exact EVR (RPMSENSE_EQUAL with a release, checked via binary_package_dependencies), so only dependents that actually go stale are rebuilt: --dependent-on kernel-image-6.18 selects every kernel-modules-*-6.18 (the same set girar's server-side kmodules verb produces), while soname consumers and unversioned runtime users (digikam linking libMagickCore, packages that merely call convert) are skipped and listed. A binary subpackage name is resolved to its source package automatically (--dependent-on libImageMagick7.10). For an update that knowingly breaks ABI (soname bump), --abi-break adds all direct runtime dependents instead — in topological build order, so inside the task each consumer rebuilds against the new library. --dry-run previews the set without touching the task; the branch comes from the task itself.
  • zoryn up: automatic version discovery can be pinned to an upstream branch — the new upstream-branch key in [version] of .gear/version-up, or the ALT kernel-team .gear/upstream-branch file (the key wins). Only tags reachable from that branch become update candidates (current-version matching is unaffected); an unresolvable branch aborts the update, and a tag younger than one hour is skipped unless --force. Kernel repositories with .gear/release-targets refuse updates on ALT branches absent from the file, and Release: resets to the branch's traditional base (alt2 on c10f2/c9f2) in up, up --switch-to-upstream-git, and spec version set. --tag bypasses the restrictions.
  • zoryn task delete / task delsub: the task ID may now be omitted — with a single PACKAGE|SUBTASK argument the command targets the last task you worked with (recorded by submit, task run ID, task new, …). Because the target task was never typed explicitly, zoryn first lists the task's subtasks, shows exactly which subtask is about to be removed and asks for confirmation (y/N, Enter aborts); -y/--yes skips the prompt and is required when stdin is not a terminal (#139).
  • task batch: new auto package mode and a global mode key in the [batch] section. In auto mode each package's spec version-release is compared with every target repository (RDB): an already-built NVR is added as a plain rebuild, a newer spec gets its own <version>-<release> tag and a source build, and repos ahead of the spec are skipped with a warning. Unknown mode values in batch configs are now rejected instead of being silently treated as build. Resolution queries RDB by the spec's own Name: — specsubst renames are not applied, and zoryn now warns once per package when a specsubst-renamed auto package is resolved. --refresh is not supported for auto-mode packages and now fails the package up front with a clear error instead of silently pushing a main-derived tag. --skip-built (test mode) now also recognises auto packages, matching build. Auto packages get gear-create-tag's default tag annotation (their own NVR) — tag_message/tag_format no longer leak a main-derived message onto their tags. An auto package listed in the config without a local checkout (its directory does not exist) is added as a plain rebuild instead of being skipped.

  • specsubst variable names and values are now validated everywhere they are taken from user input (batch configs, .gear/version-up [batch], tarball creation) before being written to git config as gear.specsubst.<name>. The error names the offending character with its codepoint and Unicode name, so a Cyrillic lookalike letter (e.g. с U+0441 instead of ASCII c) — which git rejects with a bare "invalid key" and gear then silently ignores — is explained instead of failing cryptically.

  • devenv features: new capabilities and security_opts keys in feature.toml--cap-add names and --security-opt entries applied at container-create time, flowing like devices (in the cache key only when non-empty, refused by feature add). Both are strictly validated at parse time since they reach the podman command line. The bundled podman feature now requests SYS_ADMIN (podman's default seccomp profile allows sethostname only for containers with CAP_SYS_ADMIN, and the inherited filter cannot be relaxed from inside — the capability keeps the default seccomp filter fully active, unlike seccomp=unconfined, and is confined to the container's own user namespace in rootless mode) and unmask=/proc/* (mounting a fresh procfs in a user namespace requires a fully visible /proc); with these, a nested podman run inside the devenv works end to end.
  • devenv features: new devices key in feature.toml — host device paths exposed to the podman container via --device, flowing like mounts (fixed at container-create time, so a changed device set recreates the container; a device missing on the host is skipped with a warning). The bundled podman feature now requests /dev/net/tun, which pasta — the inner rootless podman's network backend — opens to create its tap device; without it podman run inside the devenv failed with Failed to open() /dev/net/tun.
  • devenv: bundled podman feature — nested rootless podman inside the container (native overlay via host-backed storage, sub-id ranges sized to the outer user namespace); feature root run steps now execute after the dev user is created, with $USERNAME/$USER_UID/$USER_GID in scope, and after the dev user's home is chown -R'd, so anything such a step writes there stays root-owned

Fixed

  • zoryn devenv: selecting multiple features that bind-mount the same container destination no longer makes podman fail with duplicate mount destination; zoryn keeps the first mount in feature installation order. This allows the bundled opencode and pi features to share the host's ~/.agents directory.
  • task batch: specsubst = "none" in the [batch] section now disables specsubst for the whole batch, matching the per-package specsubst = "none" semantics — previously the literal string none was passed through, which was harmless for tag creation but produced spurious per-package specsubst output and, with the new auto mode, spurious RDB-name warnings.

Changed

  • zoryn task test-rebuild --package / --packages-file: the name-version form is no longer accepted — a version cannot be told from the last segment of a name (foo-1.0.0 vs perl-MIME-Base64), so a string without an -altN release is now read as a package name. Pass the bare name or the full name-version-release.
  • Branch build-dependency chains are now read from alt-releases-matrix (UpgradeChains.p_chain/c_chain) instead of a hardcoded copy in Alt_branches.build_chains, so a new branch added to the matrix (e.g. c11f1) is picked up by rebuilding against the updated ocaml-alt-releases-matrix with no zoryn code change.
  • zoryn devenv: [devenv] ports now concatenates the global list from ~/.zoryn with the project's own from ~/.config/zoryn/projects.d/<project>.toml (like mounts), instead of the project list replacing the global one — a project adds its ports to the machine's. An entry present in both layers is published once; an empty project list no longer switches the machine's ports off (there is no per-project off-switch, so the global list should carry only ports wanted by every environment).

Fixed

  • zoryn task test-rebuild --package / --packages-file: a version starting with a letter (lshw-B.02.20-alt1) is no longer swallowed by the name and skipped with not found in branch — once the -altN release is cut off the end, the segment before it is always the version (#141).
  • zoryn devenv: build-dependency resolution now expands the spec with %_sourcedir pointing at the spec's own directory (.gear/) instead of the project root, so dependency-generating macros that read auxiliary source files from %_sourcedir — notably rpm-build-pyproject's %pyproject_builddeps_*, which read pyproject_deps.json — resolve the full set instead of silently expanding to nothing. Previously a package like python3-module-cheroot entered the shell with only its statically-declared BuildRequires installed and gear-rpm -ba immediately failed on the missing generated ones. For the remaining blind spot — a copy:/copy?: rule in .gear/rules taking a file from some other directory — resolution now prints a warning naming the files that %_sourcedir-reading macros will not see.
  • zoryn gen pypi2spec: the cloned repository is now left on a sisyphus branch created from the upstream tag instead of a detached HEAD, so a subsequent zoryn submit no longer fails at the branch push step with git's The destination you provided is not a full refname error.
  • zoryn gen pypi2spec: repository URL lookup in PyPI project_urls is now case-insensitive, so packages that declare their labels in lower case ("repository", "homepage" — e.g. pyjpegls) no longer fail with No repository URL found in PyPI metadata. The homepage also falls back to the project_urls label when the deprecated home_page field is null, which is the case for anything built with modern setuptools/hatch.
  • Task manage TUI: multi-line errors from girar (e.g. a failed task run) now open in a centred scrollable popup instead of being cut off in the status line; text is word-wrapped, scrolled to the actual failure message, and can be copied with the mouse. Status messages with newlines no longer spill over the rows below the status bar (#138).
  • Interrupting a parallel build (zoryn build/zoryn up, Ctrl+C) no longer leaks the gear tarball of still-running builds into /tmp: the SIGTERM handler in forked build children now drains the temp-file cleanup registry before _exit (previously it only removed remote apt tmpdirs, and _exit skips at_exit). A forked child also drops cleanup entries inherited from the parent, so it can never delete the parent's temp files.
  • task mkrepo --clean no longer depends on girar: the task's branch is now only a hint for builder selection (a failed lookup warns and the removal runs on -b, or on the default builders), so cleanup no longer dies with Cannot determine task branch: Task <N> not found — and leaves <repo_workdir>/<task_id> behind — on hosts without a route to girar or for tasks already gone from the queue. Without a branch the host subdirectory is unknown, so both <workdir>/<task_id> and <workdir>/<host>/<task_id> are removed, but only when they really hold a generated repo (repo/rpms) — a workdir is routinely a plain home directory; --force waives the check. Generation still requires the branch.

Changed

  • Internal: TOML parsing and printing migrated from the toml (ocaml-toml) library to otoml (TOML 1.0.0 compliant, dependency-free); parse errors now carry the otoml message with line/column embedded, and user-visible behavior is otherwise unchanged.
  • task genbatch: the generated batch config is now serialized by otoml instead of a hand-written TOML emitter (escaping and section quoting come from the library; keys are indented under their section headers). The comment header and section order are unchanged.

0.48.0 - 2026-08-06

Added

  • [devenv] ports (machine config only): publish container ports to the host loopback — "8080" maps the same port both sides, "18080:8080" maps host to guest, so a dev server inside the sealed container is reachable at http://localhost:<host port>. Publishing binds 127.0.0.1 only; a host port may be mapped only once; host ports are probed before the cached container is touched, so a taken port fails with a clear message without costing you a prepared environment or leaving a half-created one; another environment of the same project still holding the port is stopped (not removed — entering it starts it again) so a routine config change does not need a manual devenv clean --all, and switching back arbitrates the other way — every path that starts a container (zoryn devenv, devenv enter, install, feature add) arbitrates first; the stop happens only after the image build succeeds, immediately before the container is created, so a rejected port list or a failed build never ends a running session; changing the list recreates the container in place (zoryn.ports label). Ignored when outbound_interface is unset, since --network host already shares the host's ports. (Danil Shein)
  • [devenv.lan] config sub-table (interface + allow, machine config only — never .gear/devenv): filtered LAN access from the devenv podman container. A pasta sidecar attaches a second tap (zoryn-lan0) to the container's namespace and routes exactly the allow-listed prefixes to the LAN interface; everything else keeps the default route and its tunnel-bound socket. The policy is validated up front (link-local overlap, default-route and host-own-address entries rejected, duplicate spellings deduplicated), the routes are reconciled into the running container on every zoryn devenv/enter/install (editing allow does not recreate the container), removing the section tears the sidecar down, and zoryn devenv inspect shows the live routes. A failed attach now tears the sidecar down instead of wedging it, and a stale sidecar pidfile is cleaned up. Setting allow without interface now warns instead of silently disabling LAN access. (Danil Shein)
  • zoryn devenv: new --timezone ZONE flag and [devenv] timezone config key (machine config only) set the podman container's timezone via podman run --tz — handy for debugging timezone-sensitive code. Changing the effective zone recreates the container from the cached image (tracked via the zoryn.timezone container label); bwrap inherits the host TZ as before. Shell completions suggest IANA zones from the host zoneinfo database.
  • zoryn devenv: new [devenv] dns, dns_search and dns_option keys and matching --dns/--dns-search/--dns-option flags — resolver settings for both the podman image build and the container. Changing them recreates the container. (Ajrat Makhmutov)
  • zoryn task new now defaults to the sisyphus repository when REPO is omitted, like ssh gyle task new (#129).
  • zoryn gitery: when [gitery] login is unset in ~/.zoryn, the login now falls back to the local part of your git user.email if it is an @altlinux.org address (e.g. ivan@altlinux.orgivan).
  • task batch: -b now accepts a pattern or comma-separated list matching several builders, and builds every package on all of them in parallel (one log per builder in .gear/), instead of warning Pattern matched N builders, using first: … and using only the first. This exists to check a batch on different architectures, not to speed it up: packages are still processed strictly one after another and each is built on every builder, so the run is never faster than on a single builder. The batch is deliberately not split between builders — packages inside it depend on earlier ones through the builder's own hasher repo, so every builder needs the complete set. --skip-built skips a package only when its src.rpm is present on every builder; with several builders the Python auto-deps rebuild retry is skipped (a warning says so at the moment it applies), and rebuild-mode packages run builder by builder (each needs its own RPM sync). While the builds run, each package prints the same live status table as zoryn build — one column per builder, a new row whenever a build stage or result changes. The table itself now lives in one place (Builder.Build_table) and is shared by both commands. A failed fan-out lists each failing builder's exit code and log path.
  • task batch: --skip-built flag (with --test) — skip packages whose exact src.rpm is already on the builder and whose git repo is unchanged since that build
  • task test-rebuild --mkrepo — build a merged task repo (as task mkrepo) for all resolved builders and run every build against it; fixes false negatives when a task renames a package that dependents pull in via a virtual Provides. The generated repo stays on disk; remove it with task mkrepo <id> --clean invoked with the same builder set. Refuses to start when a builder's build command has no {apt_tmpdir} to point at the merged repo, or when its ~/.hasher/config sets unchecked_initroot_cache — hsh then restores the cached apt box and ignores --apt-config, which would build against the wrong repository and still report success. (Aleksandr A. Voyt)
  • zoryn gitery manage: gitoskop's merge tracking against the official gears repos is now shown — a green in gears badge in the repo list marks repositories whose content is fully merged into gears, and branch/tag names already in gears are drawn green in the repo detail panels
  • zoryn gitery manage: v in the commit list toggles between the one-line view and a full git log-style view (full oid with decorations, Author:/Date: lines, indented subject); the choice sticks for the session. Ref decorations (HEAD -> branch, tag: …) are colored in both views by the same git-log syntax + theme the commit view uses. The TUI's commit/diff/file highlighting now maps theme colors to the terminal's basic palette exactly like the CLI zoryn gitery log/show/cat colouring, so both render identically (previously the TUI used muted xterm-256 approximations)
  • zoryn gitery manage: the actions menu on the repo detail screen now acts on the selected branch/tag instead of the whole repository — View log, View tree, Set as default branch (branches) and Delete branch / Delete tag (with confirmation; the deletion pushes an empty refspec over SSH, no local clone needed). Repository-level actions (rename, repack, delete repo, clone URL, charset) are offered only from the repo list, so a → Delete on a branch can no longer remove the entire repository

Changed

  • Internal: the zoryn devenv workflow moved from the CLI layer into a new devenv_pipeline library with a result-based API (errors are returned, not exited, like the other pipeline libraries); the command's behavior, flags, and output are unchanged.
  • Internal: the zoryn submit workflow moved from the CLI layer into a new submit_pipeline library, restructured as an explicit plan/execute pipeline: submission destinations (new task, --replace, --with) are resolved and validated in a preflight pass before any mutation, --dry-run renders that plan instead of running the steps inline, and execution tracks partial progress (created tags and tasks) so a failure mid-run reports what was already done. Flags and user-visible output are unchanged.
  • Internal: the zoryn builder add workflow moved from the CLI layer into a new builder_add library; the command's behavior, flags, and output are unchanged.
  • zoryn build and zoryn task batch now share one fork/poll monitor (Builder.Parallel_monitor) instead of carrying a copy of the loop each. The two copies had already drifted apart: forked children now uniformly get their own session, a /dev/null stdin, and apt tmpdir handlers that clean up before _exit, and an interrupted run waits for its children before exiting rather than returning to the prompt while they are still tearing down chroots. Ctrl-C during a parallel run now raises Sys.Break instead of exiting from inside the library, so cleanup runs: zoryn task batch saves its state and can be resumed with --continue, and the gear tarball is removed instead of being left behind.
  • zoryn gitery manage: the repo detail screen shows two panels (Branches / Tags) instead of three — the Refs panel duplicated the other two entry for entry (gitoskop's /refs is exactly branches ∪ tags) and was removed, giving both remaining panels more room

Fixed

  • the spec: path in .gear/rules is now validated to stay inside the repository — absolute paths, .. traversal and symlinked spec files are rejected (with a fallback to a root-level spec), so a malicious repo can no longer point zoryn up's spec rewrite at files outside the checkout
  • zoryn up/zoryn check: repo-declared upstream URLs (.gear/upstream/remotes, spec Vcs:/Url:) are now checked against a scheme allowlist (http, https, git, ssh and scp-like syntax) before being handed to git ls-remote — a malicious repo could declare an ext::sh -c ... URL and have git execute the command (on git builds that permit the ext transport), or a file:///local path pointing at the local filesystem
  • task rebuild/task gitclone/task genbatch and other consumers of the RDB task API: package name, version and release coming from the network are now validated at the parse boundary (Rdb.parse_task_packages_json) — a name with path separators or traversal (/home/..., ../../x) or a version/release containing / is dropped instead of flowing into download targets, clone destinations and log paths, where it could place files outside the intended directory
  • internal: ppx_shell_check now follows commands built in a prior let-binding (let cmd = sprintf ... in Shell.run cmd) — previously only the direct argument of Shell.run*/Sys.command/Unix.open_process_* was inspected. Commands are classified at the point of binding, so sanitizing a variable after the command was built no longer launders it, and every binder (match cases, let*, function parameters, type-annotated patterns) shadows outer bindings correctly. The checker is format-aware — numeric arguments need no escaping, and bare %n/%l/%L no longer shift later %s indices — and trusts internal safe generators; the handful of constructs it cannot prove are annotated [@shell_safe] with justification comments
  • sandbox: hook sandboxes (bwrap and hybrid runners) now run with --new-session, detaching them from the controlling terminal — without it, code running inside the sandbox (e.g. a malicious .gear/up.d hook) could push keystrokes into the user's shell input queue via TIOCSTI and have them executed as if typed after zoryn exits. The interactive zoryn devenv shell (bwrap backend) keeps the user's session on purpose: a new session cannot re-acquire the terminal, and the shell inside would lose job control
  • zoryn submit: squashing of a duplicate release commit now works for release subjects containing the letter t (e.g. 1.0.beta-alt1) — the old Str-based subject check treated \t in a character class as a literal t, so such subjects were never recognized as gear commits
  • internal: renamed the version library (build version string) to build_info, removing the name collision with Gau_common.Version (version comparison) and the Zoryn_version workaround alias in cmd_check.ml
  • internal: unified regex usage on the re library — girar, kernel_image, kernel_module and test_support no longer use Str
  • internal: folded the two-function cli_utils library into bin/zoryn/cli_args.ml and dropped the unused public Rdb.fetch_url re-export of Gau_http.fetch_url
  • internal: deduplicated read_file_opt (now Gau_common.Io.read_file_opt, was copy-pasted in kernel_image and kernel_module) and the task-RPM download wrapper (now Task_repo.download_task_rpms_multi, was copy-pasted in task rebuild and task test-rebuild)
  • build: dropped the unused yaml opam dependency and five unused library entries (rdb_packages, ebuild, pkgbuild, aptconf, host_monitor) from the zoryn executable's dune file
  • zoryn devenv (podman): an IPv4-mapped IPv6 address in [devenv.lan] allow (e.g. ::ffff:10.0.0.1/96) is now rejected with a clear message pointing at the plain IPv4 spelling, instead of crashing validation with an uncaught exception.
  • zoryn devenv (podman): a mixed IPv4+IPv6 [devenv.lan] allow list on an interface currently carrying only one address family no longer fails the container entry (the kernel rejects a route whose source address is not on the tap) — the unroutable entries are skipped with a warning naming them, matching the warn-and-enter behavior of the fully-unavailable case.
  • zoryn devenv (podman): installing a kernel as a build dependency no longer generates an initrd or /boot symlinks — the image now bakes /etc/sysconfig/installkernel (NOFLAVOUR=y, NODEFAULT=y, INITRD_AUTOUPDATE=none) before apt runs, so installkernel exits early; besides removing the pointless work, kernel-heavy dependency sets install noticeably faster.
  • zoryn devenv (podman): build dependencies that refuse to install outside a hasher environment (rpm-build-vm-run is not allowed outside hasher environments) now install even when the probing package is pulled in as a DEPENDENCY — e.g. a spec with BuildRequires: rpm-build-vm pulls rpm-build-vm-run, which the old check did not see because it only matched explicitly requested names. The /.host, /.in, /.out marker directories are now created before every in-container apt install.
  • zoryn submit: creating the package repository on gitery no longer fails for packages removed from sisyphus or last built from an srpm (#133). The gears/srpms source path is now resolved through the RDB clone_url API (like zoryn clone) instead of guessing from the last build task — a rebuild-type task used to be misread as gear-based, pointing the clone at a nonexistent /gears repo. A failed ssh gitery clone is no longer silently ignored ("Repository may already exist, continuing..."): submit now falls back to ssh gitery init-db and pushes the full local history, and only errors out when both fail.
  • zoryn devenv (podman): re-entering a stopped cached environment no longer re-resolves the package's BuildRequires every time. The recorded digest that decides whether the spec changed lives inside the container and is read with podman exec, which fails while it is stopped — reading as empty, and so as "BuildRequires changed". Every entry after a reboot or a switch between environments therefore ran a full dependency resolution, and failed outright offline, for a spec that had not changed. The container is now started before its digest is read. (Danil Shein)
  • ~-expansion of configured paths is now one shared implementation instead of a family of drifted copies (shell, bwrap and podman devenv mounts, sandbox, batch configs, readline completion, [commands] values, and the devenv command itself): no doubled slash when $HOME ends with / (the doubled path could leak into devenv cache keys and needlessly invalidate a cached environment), and ~user-style paths are consistently left untouched everywhere instead of being mangled by some copies.
  • task batch: an existing task whose main-package subtask is registered under the gitery repo name (e.g. php.git=8.5.9-alt1 for a [php] entry with specsubst = "none") is found again. Since the specsubst-rename fix the task search matched only the spec Name: (php8.5), but task ls lists subtasks by repo basename, so such tasks were reported as "No task found". The search now tries both the spec name and the config/directory name, each with the batch tag and the bare version-release.
  • SRPM lookup in srpms_path: packages whose version starts with a letter (gnustep-* r36614, lshw B.02.20, retroarch-assets git20220113, ~95 sources in p11) are found again. The SRPM filename matcher required the version to start with a digit, both in the local directory scan and in the remote find -regex search, so task test-rebuild failed instantly with SRPM not found, task rebuild skipped the package with a No src.rpm found matching pattern warning, and task batch in rebuild mode aborted with No SRPM found. The version may now start with a letter as well as a digit; cross-package prefix matches are still rejected by the anchored -alt<release> structure of the pattern. (Aleksandr A. Voyt)
  • zoryn devenv now operates on the git repository root when run from a subdirectory (e.g. .gear/), instead of treating the subdirectory as the project — running from .gear/ used to produce an invalid image tag (zdevenv-.gear-…: invalid reference format) and made devenv clean/list miss the project's environments. Outside a git repository the current directory is used as before. The image-tag slug also drops leading non-alphanumeric characters, so a dot-directory project name can never produce an invalid OCI reference.
  • zoryn devenv (podman): builds no longer die with posix_spawnp: Resource temporarily unavailable[devenv] pids_limit set only the cgroup pid cap, leaving podman's much lower RLIMIT_NPROC default (ulimit -u ≈ 512) as the limit parallel builds hit. It is now passed as --ulimit nproc too, clamped to the host's hard limit. Takes effect on --rebuild.
  • zoryn devenv (podman, outbound_interface): a dual-stack tunnel interface no longer fails container creation with pasta's External interface not usable when the default-route template lacks one of the families (e.g. no global IPv6 on the LAN interface) — the egress binding now covers only the families present on both interfaces, keeping the container single-family as documented. (Danil Shein)
  • zoryn up: with a .gear/version-up pattern carrying a name prefix (e.g. Kea-{major:+}.{minor:+}.{patch:+}), the spec Version: is again the bare version. The auto-discovery path took the whole tag as the version and only replaced its separators with dots, so Kea-3.2.0 became Kea.3.2.0 and gear-update-tag then looked for a tag named Kea-Kea.3.2.0 and failed. The literal parts of the pattern are now stripped, as everywhere else.
  • zoryn submit now derives the gear tag name from git config gear.create-tag.name (default @version@-@release@, with @name@/@version@/@release@ expanded from the spec Name:), exactly like gear-create-tag. A stale tag of a different shape sitting on another commit is no longer force-recreated under a name gear would never produce — a differently shaped tag is only reused when it is already on HEAD. --dry-run now reports what the real run will do (Would reuse existing tag: … when a tag for this NVR is on HEAD, otherwise the name that will be created); previously it always printed <pkg>-<version>-<release> regardless of the configuration and of what the run would actually do (#131).
  • zoryn gitery manage: in the repo detail screen, the Branches/Tags/Refs panels now scroll with the cursor — with more entries than the panel can show, moving the selection below the last visible row used to leave the cursor drawn nowhere. PgUp/PgDn/Home/End also page by the panel height instead of a stale full-screen page size.
  • zoryn devenv: the timezone key is now accepted by the [devenv] config schema — setting it no longer prints unknown key 'timezone' in [devenv] on every run.
  • zoryn submit --replace=TASK_ID:N now honours the explicit subtask number unconditionally, in every mode: the numbered subtask is replaced even when its git repository name does not match the submitted package (a warning is printed in that case); a :N naming a subtask that does not exist in the task is an error. Previously the subtask was silently looked up only among subtasks of the current package, so replacing a subtask of another package was impossible. In kernel/batch-specsubst mode the explicit number overrides value pairing (accepted only when a single tag is submitted; several tags with :N are rejected). The pairing itself, used when no :N is given, now matches kernel-image subtasks by the package name in their tag rather than by git repository name, so a subtask built from the official gears repo (/gears/k/kernel-image-<flavour>.git) is found from a locally-named clone (e.g. kernel.git). Girar's transient task lock (task add: task #N is locked, held briefly while girar processes the task) is retried a few times instead of failing at once, and a lock that persists aborts the replace before task delsub's removal could be followed by a failed task add.
  • A builder whose builders.d file has no [commands] build now builds with the same command every other part of zoryn reports for it. Builds used a private fallback template that omitted --apt-config (silently ignoring the builder's repo setting) and, on a remote builder, the tarball path as well; zoryn builder config printed a third, placeholder string. All three now come from one place. Note for builders without repo: the default command now passes --apt-config=$HOME/hasher_<N>.env/<branch>/apt.conf — the config zoryn builder add writes. A hand-written builders.d entry that never went through builder add needs that file to exist (or an explicit [commands] build); previously such a builder built against hasher's own apt defaults with no --apt-config at all. Note for builders with repo: the default command now contains {apt_tmpdir}, so the builder's apt config is rendered into a temporary directory before each build (a mktemp and an rsync per build on a remote builder) — the same thing that already happened for anyone whose [commands] build spelled {apt_tmpdir} out. (Aleksandr A. Voyt)
  • zoryn task mkrepo no longer aborts with a raw Sys_error when the apt config next to the generated repo cannot be written (an aptconf path occupied by a file, a full or read-only filesystem). The write is best-effort by design — the repository is already built at that point — but the failure escaped as an exception instead, so the run died without ever printing the repo URL. It now warns and finishes. (Aleksandr A. Voyt)
  • Python auto-deps: the guard that aborts the pyproject_deps.json update now checks only the sources named in the build log. It checked every source in the file, so an unrelated one emptied by the resync (pep517 when upstream ships no pyproject.toml) rolled back the copied file and left the package unbuildable.
  • zoryn check version and the no-watch-file git-tags fallback of zoryn up now honour the [version] pattern/template from .gear/version-up. Both previously took the generically "latest" remote tag, which could be a non-release tag — llama.cpp reported 10103 -> 9794052.
  • Failures on those two paths are no longer reported as "Already up to date!". An unreachable remote (the message now carries git's own ls-remote error), a pattern matching no tag (the message names the pattern), a remote with no tags or no version-shaped tags, tags yielding no comparable version, and a filter naming a group the pattern never captures are all errors now. Having nothing to compare against yet — a [version] filter upstream has not published a matching tag for, or only pre-releases so far — remains an ordinary result, and zoryn up now says which of the two it is instead of printing "Already up to date!".
  • zoryn check version no longer stops at a broken watch file when the spec also carries a git URL: if the watch file cannot be parsed or its upstream listing comes back empty, the remote git tags are consulted instead (with a warning saying why). Previously the presence of a watch file suppressed the git check entirely, so a package whose upstream moved its archive index — ImageMagick, whose imagemagick.org/archive/ is now a redirect page — was reported as up to date while real releases were being missed.
  • zoryn check version exits 1 when the check itself fails (remote cannot be listed, watch file unparsable or empty, PyPI lookup error, broken [version] config) instead of always exiting 0, so scripts and CI can detect it. It also prints which tag it selected.
  • A group written by hand in a [version] pattern (e.g. (mysql|redis)-{major:+}.{minor:+}.{patch:+}) no longer captures a value, so it can no longer shift the placeholders onto the wrong groups: mysql-1.2.3 used to bind major to mysql, yielding a version rpmvercmp could not compare and a permanent false "up to date". A named group ((?<x>…)), which does capture, is rejected for the same reason, as are lookarounds and inline flags — the regex engine does not implement them, and they now say so instead of failing as a generic invalid pattern. (?:…) is unaffected. A malformed pattern fails with a readable message instead of an internal Re__Parse_buffer.Parse_error.

0.47.0 - 2026-07-23

Added

  • zoryn gitery: new command for browsing and administering gitery repositories — ls, find-package, log, commit, diff, show, refs, branches, tags, tree, cat (gitoskop HTTP API, --json output), plus SSH server verbs quota, clone, charset, default-branch, init-db, repack, mv-db, rm-db (mutating verbs prompt for confirmation unless -y/--yes). Shell completion for repository (own repos for rm-db), branch/tag names and -n namespaces; -d/--debug logs each request. Adds [gitery] login and [gitoskop] url to ~/.zoryn.
  • zoryn gitery log / diff --patch / show: colourised, paged output like git log --decorate / git diff on a TTY; honours NO_COLOR.
  • zoryn gitery find-package: full match set via paged fetching (client cap 10000) instead of only the server's first 50; -n/--namespace NS to restrict to a root.
  • zoryn gitery manage [REPO]: interactive full-screen TUI for browsing and managing your gitery repositories — repo list with quota/size/date, sort and search; per-repo branches/tags/refs, commit log, show + diff, file tree; administrative actions (charset, default branch, repack, rename, delete, clone URL) plus mark-and-bulk-delete; Tab/p to view another user's namespace read-only.
  • submit --with: TAB now suggests the remembered last task id (#121).
  • zoryn devenv --feature ID / -F ID (repeatable): one-shot additive enable of a devenv feature for a single run, on top of config/profile features, without editing config.
  • zoryn devenv feature add ID...: inject devenv feature(s) into the current running podman container in place (like devenv install for packages), without recreating it. Podman-only; mount-bearing features are refused with guidance to use -F.

Fixed

  • gitery manage: Esc on the repo list with an active search filter now cancels the search instead of quitting the TUI; the bottom hint reflects it while a filter is active.
  • gitery manage: the actions menu's Delete now bulk-deletes every marked repo when at least one mark is set, matching the Delete key (and its own help text). It previously always deleted only the repo under the cursor, ignoring the marks.
  • gitery manage: a successful repo delete no longer reports "delete FAILED". Post-delete verification reloaded the repo list through the gitoskop HTTP API, whose server-side index lags behind rm-db, so the just-deleted repo still appeared in the listing. Verification now uses a targeted ssh gitery ls of each repo's own directory, which reflects the deletion immediately; confirmed-deleted repos are also dropped from the on-screen listing right away instead of lingering until the gitoskop index catches up, and gitoskop is asked to re-read the deleted repos' data (?reread_data=1) so subsequent reloads see the fresh state at once.
  • task rebuild --parallel: builders sharing an architecture are now deduplicated to one per arch before building (#124).
  • task batch: a subpackage already present in the task is no longer reported as a failure; the "already added" reply from gyle is now recognized anywhere in the line.
  • task batch: find an existing task when the main package's spec Name: differs from its config/directory name (e.g. phpphp8.2).
  • up, spec add changelog: decode HTML entities in Mozilla advisory CVE titles, so Copy &amp; Paste no longer reaches the spec changelog.

0.46.0 - 2026-07-12

Added

  • task refresh: detect subtasks built for the branch from a different source
  • check upstream: scans the local worktree first when run in the package's gear repo.
  • check upstream: uses watch file named via copy: in .gear/rules/.gear-rules.
  • check upstream: recognizes bare watch copy names (e.g. altlinux/watch).
  • clone: configurable clone_url API endpoint via [clone] api_url in ~/.zoryn.
  • --verify-failures now re-verifies stalled builds too, not only failed ones: a task RPM can introduce a hang, so a package that stalls with the task but builds cleanly without it is reported as a regression, and one that fails without it as pre-existing. A build that stalls in verification as well stays unresolved and is re-verified on --continue.

Changed

  • clone: remove the [clone] url config; the repo URL now comes from RDB clone_url (#114).

Fixed

  • task batch: TAB now completes the CONFIG argument (named configs + file/dir paths).
  • clone: resolve repos via RDB clone_url API — clones packages lacking task history (#114).
  • gen environment: generated local build command now passes {hasher_dir}, keeping build, check packages, and builder shell on the same hasher directory (#120).
  • docs site: landing-page burger now opens a mobile nav menu instead of doing nothing.
  • docs site: TOC renders as a classic tree; sticky title no longer overlapped when scrolled.
  • docs site: burger now appears below 1220px, fixing a dead zone where nav vanished (#116).
  • docs site: TOC scrolls the current section into view; drawer TOC kept flat (no tree).
  • docs site: set site_url — fixes broken sitemap URLs and emits canonical links (SEO).
  • docs site: RU/EN landing get proper meta descriptions with search keywords.
  • check upstream: .gear/watch is now probed even without a .gear/rules file (#118).
  • build: parallel progress table no longer shows stages from a previous build's stale log.
  • docs: gen opam2spec no longer lists nonexistent -p/--packager and --build options.
  • docs: check upstream synopsis drops the unsupported -B <repo> option.
  • docs: task download option corrected from -b to the real -B/--branch.
  • docs: task rebuild -b documented as builder pattern, not a target-repo flag.
  • submit --help: examples use the real -B branch flag instead of the invalid -b.
  • docs: gen opam2spec drops bogus --verbose, documents real --get-version/--sync.
  • docs: gen opam2spec --tag-prefix default is auto-detection, not the literal v.
  • docs: gen opam2spec -o/--output defaults to .gear/<name>.spec, not stdout.
  • docs: check upstream output described accurately (watch file/source/VCS/upstream git).
  • docs: [ssh] persist default corrected to 10m (matches the code).
  • docs: document [ssh], [clone], [repoteka], [hosts], [build] packager config keys.
  • docs: task copy points to task manage for deps, not the nonexistent task deps.
  • docs: task test-rebuild documents -B, -p, -s, --deps-by-pkg, --log-dir.
  • docs: submit documents the --no-edit-commit flag.
  • docs: gen version-up documents short forms -t/-f for --tag/--force.
  • docs: task genbatch documents short -o for --output.
  • docs: task batch documents --no-python-auto-deps and --no-edit-commit.
  • docs: builder run documents short -e for --execute.
  • config: drop dead [batch]/[specsubst]/[version] entries from the ~/.zoryn schema.
  • docs: build/up synopsis lists --no-python-auto-deps.

0.45.0 - 2026-07-07

Added

  • devenv enter without CONTAINER (or .) enters the current directory's container.
  • devenv install --container installs straight into an existing podman container.
  • test-rebuild: builds killed by hasher-priv wlimit_* limits are stalled, not failed.

Changed

  • gen environment: packager name/email prompts now explain their use and show examples.
  • build: alt_releases_matrix >= 0.2.8 is required (known_branches API rename).

Fixed

  • spec version set: release is now reset to alt1 when the version changes (#115).
  • docs site: page TOC restored in the mobile drawer; sidebars now labeled (navigation / contents).
  • docs site: desktop sidebars get their own scroll — long TOCs no longer clipped on short windows.
  • docs site: search results are filtered to the current language — no more English hits on RU pages.
  • devenv: empty apt sources (e.g. blank /etc/apt/sources.list) warn and keep container defaults.
  • devenv: invalid apt sources errors now name the offending file/builder/inline config.
  • devenv enter/remove/inspect: CONTAINER and --user tab completion now works.
  • devenv --mount-bind: relative host paths are resolved against the current directory.
  • submit -B: comma-separated branch completion works again (was shadowed by devenv -B).
  • submit --replace/--with: step-7 upstream deps are now applied to the reused task.
  • submit --replace=TASK_ID: rejects a task outside the -B target repository.

0.44.0 - 2026-07-03

Added

  • task manage: o/O keys insert a subtask after/before the current row (#111)
  • zoryn task batch --batch-pkgs PKGS builds only the named packages from the batch config (comma-separated), mirroring zoryn build --batch-pkgs. Names must exist in the config or the run aborts listing the available packages; selection keeps config order and combines with --skip, which further excludes from it. Shell completion suggests package names read from the CONFIG argument (comma-aware, dropping already-typed names).
  • zoryn devenv --mount-bind SPEC bind-mounts an extra host path into the dev environment for a single run, on top of any [devenv].mounts from ~/.zoryn. The value uses the same syntax as that config key: a bare PATH binds read-write on the same path both sides, HOST:CONTAINER remaps the container path, and a trailing :ro makes the mount read-only (HOST:CONTAINER:ro); a leading ~ expands to $HOME. The flag is repeatable. With the podman backend the mount is folded into the container cache key, so adding or changing one recreates the container (image layers are reused); with the bwrap backend it is bound at enter time (--bind / --ro-bind), attaching to the existing chroot without rebuilding.
  • New inactivity_timeout field in [build] section of ~/.config/zoryn/builders.d/*.conf: if a running build produces no new log output for this many minutes (default: 60), it is killed as stalled. The clock starts only after the build writes its first byte to the log, so a long silent setup phase is not mistaken for a hang. Set to 0 to disable. Can also be set once in the global ~/.zoryn [build] section as a fallback for builders that do not define it. Displayed by zoryn builder config. The watchdog runs only for parallel builds (2+ builders, no --sequential); zoryn warns when the field is set but the run is sequential.
  • New max_log_mb field in [build] section of ~/.config/zoryn/builders.d/*.conf: if a build log grows past this size in MiB (default: 1024 = 1 GiB), the build is killed as stalled — catches builds stuck in a tight output loop. Set to 0 to disable. Can also be set once in the global ~/.zoryn [build] section as a fallback for builders that do not define it. Displayed by zoryn builder config. Like inactivity_timeout, the watchdog runs only for parallel builds (2+ builders, no --sequential).
  • Stalled builds now show a distinct stalled status in rebuild-stats.json (instead of failed), making them easy to identify and rerun with --continue. A run that leaves any stalled build exits non-zero, so automation never mistakes an incomplete rebuild for success.
  • The [build] timeout builder setting (how long to wait for a free builder slot) can now also be set in the global ~/.zoryn [build] section, used as a fallback when a builder does not define it — matching inactivity_timeout and max_log_mb.
  • zoryn submit --no-deps skips auto --deps on tasks in fresher repos. Closes #106.
  • Log highlighting: pytest-style PASSED verdicts are shown in green.

Changed

  • The [devenv].mounts config (and --mount-bind) now also apply to the bwrap backend, not just podman — each spec is bound into the chroot at enter time via --bind / --ro-bind (read-only when the spec carries :ro). Previously extra mounts were silently dropped for bwrap.
  • zoryn task genbatch now recognises gear specsubst groups (several subtasks built from one git repo, e.g. dune/dune-ordering/dune-dyn from dune.git) and emits them as specsubst variants with a shared relative path and specsubst = var=value (value taken from the tag suffix, base gets the empty value). The specsubst variable name is fetched from the repo's .gear/rules over gitweb; when it cannot be resolved (or no base package is present) the members are listed flat with a note on stderr, and must have their path/specsubst set by hand before that group will rebuild. Generated configs are now valid TOML — all string values and dotted table names are quoted (previously emitted unquoted, so the output did not parse).
  • Mozilla CVE lookup for changelog generation now scrapes the mozilla.org security-advisories website (https://www.mozilla.org/en-US/security/advisories/) instead of the retired mozilla/foundation-security-advisories GitHub repository, which Mozilla is phasing out and which had begun serving mismatched advisory data. The index page maps <product> <version> to an advisory (MFSA) slug, and that advisory page is parsed for its CVE IDs. Advisories that fix several releases in one index entry (comma- or "and"-separated, e.g. Firefox 136.0.4, Firefox ESR 128.8.1) are matched on any listed <product> <version>.

Fixed

  • builder: silenced cd: ...: No such file noise from hsh --printenv calls.
  • devenv bwrap: sources.list-format apt_config is wrapped in a generated apt.conf for hsh.
  • devenv bwrap: --with-builder-repo now passes an apt.conf file (not a dir) to hsh.
  • Log highlighting: error/fail/exception no longer lit inside words like TypeError-None. Closes #110.
  • zoryn devenv no longer runs podman on hosts where it is not installed. Closes #107.
  • devenv --help: no more cmdliner "unescaped '$'" errors on stderr. Closes #108.
  • task manage: removing a dependency now really removes it. Closes #105.
  • task manage: deps screen no longer shows a stale deps list from another task.
  • task manage: adding a dependency uses girar task deps add, so edits can't be lost.
  • task manage: deps screen reports load failures honestly and supports R/F5 refresh.
  • task manage: after girar actions the task is re-read bypassing the API cache.
  • zoryn up / zoryn spec with parser = "oracle-csaf" no longer adds CVEs whose fix is in a version newer than the update target. Oracle CSAF emits one entry per affected product_id, so a single CVE that touches multiple version lines (e.g. 8.0.0-8.0.45 and 8.4.0-8.4.8) becomes several vulnerability records, each with a narrow affected_versions range. The old collect_cves filter checked every record in isolation: updating 8.0.44 → 8.4.8 matched the 8.0.0-8.0.45 record (old in range && new not in range) and reported the CVE as fixed, even though the parallel 8.4.0-8.4.8 record showed 8.4.8 is still vulnerable — the actual fix is in 8.4.9. As reported, 40 CVEs from the January 2026 Oracle CPU (fixed in MySQL 8.4.9) were appended to a 8.4.8-alt2 changelog entry. collect_cves now groups every vulnerability entry by CVE id (after the product filter), concatenates all affected_versions ranges, and only keeps the CVE when old_version is in some range and new_version is in none — so a target below the CPU's fix is correctly recognised as still vulnerable and the CVE is dropped.
  • The Oracle CSAF changelog source now falls back to older quarters when the predicted advisory is not published yet. zoryn up predicts the current-quarter CSAF URL from today's date (e.g. in July it asks for cpujul2026csaf.json), but Oracle publishes Critical Patch Updates quarterly and the current quarter's file may not exist for weeks — the fetch failed with HTTP error 404 and no CVEs were collected. On a 404 the fetcher now walks back one quarter at a time (jul -> apr -> jan -> prior oct, up to a year) until it finds a published advisory, emitting a warning for each step. Non-404 errors (timeouts, HTTP 5xx) and URLs that do not follow Oracle's cpu<quarter><year>csaf.json naming are left unchanged.
  • Shell completion no longer leaks ANSI escape codes into descriptions, which corrupted the zsh completion menu (entries such as replace and debug appeared truncated). The bash/zsh/fish scripts now invoke zoryn --__complete with TERM=dumb, so cmdliner emits plain text instead of SGR-styled doc strings. Closes #98.
  • zoryn devenv no longer rebuilds the podman image when only the bind-mounts change. Bind-mounts are runtime -v flags applied at container-create time and never affect image content, but they were folded into the image tag, so adding/changing a mount (--mount-bind or [devenv].mounts) produced a new tag and re-ran podman build. The image tag now ignores mounts (only the container name distinguishes mount sets), and the image build is skipped entirely when the tag already exists — so a mount change recreates just the container and reuses the existing image.
  • zoryn task batch --test now writes the build log again. Local test builds ran with no log file at all, so hsh output was streamed to the PTY and discarded — only stale logs from earlier non-test runs remained on disk. The log is now written to .gear/<log-filename> inside the package directory, the same place run_step_build uses. The filename now feeds the specsubst value into the {batch} template variable (as zoryn build does), so each variant of a specsubst group gets a distinct log (e.g. build.ordering.<builder>.log) instead of every variant overwriting build.<builder>.log. The path is also fixed to be relative to the package directory the step already chdir'd into: it previously re-joined pkg_path, so a relative --basedir double-nested the log under <pkg>/<pkg>/.gear/ where nobody looked for it.
  • zoryn task rebuild and zoryn task test-rebuild now kill the entire process group when a build worker is interrupted or torn down, so descendant processes spawned by the local build tool (hsh, gear, etc.) cannot outlive the worker.

0.43.1 - 2026-06-30

Changed

  • The installed zoryn Agent Skill reference (the pointer zoryn agent skill install writes into ~/.claude/skills / ~/.agents/skills) now embeds self-contained "build inside a devenv" guidance directly, instead of relying only on the full skill under /usr/share/zoryn/. Inside a zoryn devenv that system path does not exist (the container has no zoryn package installed), so an agent following the pointer could not reach any build guidance. The pointer now tells the agent to detect a devenv by the devenv- hostname prefix and build locally with git add -A then gear-rpm -ba --commit >/tmp/build_log 2>&1 (reading the log for the result), and that gear-rpm builds under ~/RPM/ — the standard rpmbuild tree (BUILD/, RPMS/, SRPMS/, …) — for debugging a failing %build/%check step. The same guidance was also extended in skills/zoryn/SKILL.md and skills/zoryn/references/devenv.md.

Fixed

  • zoryn builder shell now checks the builder's hasher chroot (<hasher_dir>/chroot) before launching and, when it is missing, refuses with a clear message naming the builder and how to create it (hsh --initroot-only <hasher_dir>, or run a build). Previously hsh-shell silently fell back to the workdir in ~/.hasher/config (often under $TMPDIR) and failed with a baffling cd: ...: No such file or directory.
  • zoryn devenv now works for domain (LDAP/AD/SSSD) users whose UID/GID is very large. Previously the image build baked the literal host UID into the dev user, which failed for ids above 60000: ALT's useradd could not create the per-user tcb shadow directory (Failed to create tcb directory), and a rootless podman build could not chown to an id outside the user's subuid range (chown: changing ownership of '/home/<user>': Invalid argument), so the build aborted at the ~/.bashrc step with /home/<user>/.bashrc: No such file or directory. zoryn now bakes the dev user at a fixed in-range id (1000) when the host id is out of range and remaps the real host id onto it at runtime with --userns=keep-id:uid=1000,gid=1000, so bind-mounted project files stay owned by the real host user. In-range ids keep the previous 1:1 --userns=keep-id mapping unchanged. The build also now guarantees the home directory exists (mkdir -p + chown -R) regardless of useradd's outcome, creates the dev user with useradd -o/groupadd -o (non-unique) so a fallback id already present in the base image does not break the build, sanitizes the baked user name (a qualified domain login like jdoe@corp.example.com or CORP\jdoe becomes devuser, since useradd rejects such names and a . in the name makes sudo silently ignore the sudoers.d drop-in), and no longer discards groupadd/useradd stderr, so any future failure is visible in the build log. Closes #97.
  • zoryn build no longer fails with cd: <workdir>: No such file or directory when the local [build] command passes no explicit workdir argument. Such a command lets hsh pick its workdir from its own config (~/.hasher/config, which may point under $TMPDIR, e.g. /tmp/.private/<user>/hasher), but zoryn only created its own hasher_dir (~/hasher) — so hsh tried to cd into a directory that was never created. zoryn now asks hsh --printenv for the effective workdir and creates that directory when the build command has no {hasher_dir} of its own; builds whose command does pass {hasher_dir} are unchanged. Closes #96.

0.43.0 - 2026-06-26

Added

  • zoryn up --no-gpg-sign makes zoryn's own automated commits (the source-update commit, merge conclusions, the switch-to-upstream-git file move, and release-commit squashing) with git commit --no-gpg-sign. Lets zoryn up run in environments without gnupg2, or where commit.gpgsign is enabled but no signing key is available, instead of failing with cannot run gpg2: No such file or directory. Off by default; your own commits made with zoryn commit are unaffected.
  • New devenv feature GLM-claude-code — a clone of the claude feature that installs the same Claude Code CLI but mounts a separate profile: host ~/.config/claude-glm → container ~/.config/claude and host ~/.claude-glm.json → container ~/.claude.json. Lets a GLM login/config live alongside the default claude profile without overwriting it.
  • .gear/version-up template now supports default values: {patch:0} emits the captured group or 0 when it is absent. Lets one config keep a fixed version layout when upstream sometimes drops a component (e.g. v5.8-5055.8.0.505, v5.8.1-5065.8.1.506), so versions stay monotonic instead of an omitted segment shifting build into the patch slot.
  • zoryn devenv now sets the terminal window/tab title while the dev shell is built and used — Zoryn: devenv <host>: <spec> — so it is visible on which host zoryn devenv is running and for which spec file (falling back to the project directory name when no spec is found). The title is restored on exit.
  • New zoryn devenv profile list command — prints the names of every [devenv.profiles.<name>] profile defined in the machine config (~/.zoryn) and the per-project local config, one per line. With --verbose it prints each profile's effective [devenv] configuration instead — the resolved keys and features, merging the machine and per-project local configs. Shell completion for zoryn devenv --profile now calls this command instead of parsing ~/.zoryn itself, so <TAB> offers the real profiles (including project-local ones) without duplicating the config-parsing logic in the bash/zsh scripts.

Fixed

  • zoryn builder shell / zoryn build mountpoint auto-fix no longer tells you to put device nodes (e.g. /dev/kvm) into hasher-priv's allowed_mountpoints. hasher-priv rejects device nodes there with lookup_mount_entry: /dev/kvm: mount point is not supported, so following the hint (or the sudo auto-fix) broke hsh-shell/hsh. Entries are now classified: real mountpoints (/proc, /dev/pts, /dev/shm, /sys) go to allowed_mountpoints, while device nodes (/dev/kvm, /dev/net/tun, …) go to allowed_devices. The fix hint and the sudo command emit the correct key for each group. zoryn builder shell now checks the mountpoints against hsh --printenv (known_mountpoints) — the authoritative set hasher itself resolves, which also works when /etc/hasher-priv/system is not directly readable — and, since the builder is already configured, only warns with a recommendation instead of refusing to open the shell. zoryn builder add still attempts the sudo auto-fix, but on failure (including an unreadable /etc/hasher-priv/system) it prints the allowed_mountpoints=/allowed_devices= keys to add by hand and continues adding the builder. Device nodes are not listed in known_mountpoints, so they appear only in the recommendation. (Closes #95)
  • zoryn devenv argv reordering now recognises the profile sub-command, so zoryn devenv profile list reaches the right command. The option-hoisting helper scanned for the first known devenv sub-command name and, not knowing profile, skipped it and hoisted the inner list token to the devenv level — running devenv list with a stray profile argument instead.
  • zoryn up --switch-to-upstream-git now force-stages .gear/ with git add -f at every staging point — the Step 5 file-move commit (spec, rules, and any moved patch files), the rewritten .gear/rules during the unrelated-histories merge, and the .gear/version-up scheme override written at the end. In repos whose .gitignore matches .gear (e.g. openimagedenoise ignores all dotfiles via .*), the switch previously failed with git add (extra stage paths) failed / The following paths are ignored by one of your .gitignore files: .gear.
  • zoryn up --switch-to-upstream-git now writes a templated, self-excluding tar: directive in the rewritten .gear/rules: tar: v@version@:. exclude=.gear/** instead of the literal resolved tag (tar: v2.3.3:.). The version part of the tag is replaced with @version@ so future zoryn up substitutes the current version, and exclude=.gear/** keeps the merge commit's ALT-specific files out of the generated tarball (a second exclude=<subdir>/** is added when a preserved subdir like altlinux/ exists).
  • zoryn gen-pypi2spec, zoryn gen-opam2spec, and zoryn gen-watch now stage their generated .gear/ files (.gear/rules, the spec, .gear/watch) with git add -f, so generation works in repos whose .gitignore matches .gear.
  • security: ppx_shell_check no longer accepts a mixed escaped/raw concatenation as safe. The old contains_escape heuristic flagged an argument as safe when any sub-expression contained an escape/quote call, so Printf.sprintf "git checkout %s" (escape tag ^ host) passed the check while leaking the raw host into the shell. The new is_shell_safe predicate requires every leaf of a ^/String.concat expression to be provably safe (escaped, a literal, or String.concat sep (List.map escape …)). A standalone-driver regression test (test/dune runtest rule on ppx_bad_mixed_concat.ml) now asserts the bypass is rejected at build time.
  • Devenv TOML parsing (.gear/devenv, ~/.config/zoryn, project-local configs) now prints a warning with the line and column on a parse error instead of silently treating the file as empty. A single malformed entry (e.g. a mismatched quote) used to make zoryn ignore the whole packages list with no diagnostic, so configured packages never reached the dev environment.
  • zoryn up (automatic tag discovery) now derives the new spec version through the version-up template, matching the alias tag it creates. Previously the spec version came from raw git-tag parsing while the alias used the template, so a reshaping template (optional segment or {patch:0} default) produced a spec version that disagreed with the alias tag and gear-update-tag failed with Tag "…" not found. The explicit --tag path was already correct.
  • Sandbox (bwrap and hybrid runners) now pass the build script and its arguments to the inner bash -c as positional parameters ("$@") instead of interpolating them into the single-quoted command string. The old form let the inner shell re-parse already-escaped text, so a repo path containing a single quote or other shell metacharacter could break the quoting.

0.42.0 - 2026-06-24

Fixed

  • zoryn check version and zoryn check upstream no longer require --package — when omitted, the package name is read from the Name: field of the .gear spec in the current directory, matching the documented default. Closes #91.
  • zoryn up on a PyPI-backed package now prints a note when PyPI is up to date but a newer final-release git tag exists locally, pointing at zoryn up --tag <tag> to merge it, instead of silently reporting "Already up to date". Closes #93.
  • zoryn task add now accepts an omitted TASK_ID and targets the last task worked with, so zoryn task add copy <pkg> <repo> works right after task new instead of failing with invalid TASK_ID: copy. A successful task add also records the task.
  • zoryn task run/task show without a task ID now reuse the last task zoryn worked with instead of gyle's highest-numbered task. Commands that target a concrete task (submit, task run TASK_ID, task new) record it in $XDG_STATE_HOME/zoryn/last-task; the omit-the-ID forms read that first and only fall back to gyle's latest when nothing is recorded. Closes #90.
  • zoryn up on kernel-style specs no longer overwrites Version: %kversion with a literal version. When the version is composed by concatenating macros without a joining dot (e.g. %kernel_base_version%kernel_sublevel with kernel_sublevel = .35), it now walks the template positionally and bumps the right leaf macro (%kernel_sublevel.36), leaving the Version: macro reference intact. The macro line's original indentation and name/value alignment (tabs included) are preserved.

Added

  • zoryn devenv bundled kimi feature: installs the Kimi Code CLI (MoonshotAI) into ~/.local/kimi and bind-mounts ~/.kimi-code so login, config and sessions persist.
  • Bundled zoryn Agent Skill now tells agents that inside a devenv (hostname devenv-*) the package is built locally with git add -A + gear-rpm -ba --commit rather than zoryn build/hasher, since the BuildRequires: are already installed.
  • devenv --with-task-repo <id>: install packages from a gyle task's built RPMs inside the dev environment (downloaded into a local cache, exposed as an additive apt-rpm rpm-dir source).
  • devenv --with-builder-repo <builder>: install packages from a builder's hasher output repo inside the dev environment (additive apt-rpm rpm-dir source; podman and bwrap).
  • zoryn submit -m/--message sets the build reason passed to gyle when the task runs, overriding the auto-detected reason for stable branches (and adding one on sisyphus). -m - reads the message from stdin, mirroring zoryn task run.
  • devenv podman: generate the container's apt sources.list from a builder, apt-config path, inline apt_sources, or --apt-conf, replacing the base image's sources; local file: repositories are bind-mounted read-only.
  • zoryn devenv SSH agent forwarding (podman): --ssh-agent/-A or [devenv].forward_ssh_agent = true bind-mounts the host $SSH_AUTH_SOCK into the container (fixed /run/ssh-agent.sock + SSH_AUTH_SOCK env), so agent keys — including those forwarded by an ssh -A login — work inside. The socket is out of the cache key; the container is recreated when the socket changes between logins.
  • zoryn devenv bundled ssh feature: installs openssh-clients and bind-mounts the host's ~/.ssh read-only, so keys, config and known_hosts are available inside (e.g. for git-over-ssh and remote builders).
  • zoryn devenv bundled zoryn feature: installs the zoryn package from the container branch's repo and bind-mounts the host's ~/.zoryn and ~/.config/zoryn (builders, settings) read-only, so zoryn runs inside the dev environment against your host configuration. The package ships its Agent Skill under /usr/share/zoryn/skills/, so no skill mount is needed.
  • zoryn agent skill manages the bundled zoryn Agent Skill (agentskills.io open format) for local AI agents. zoryn agent skill install writes a small reference SKILL.md — the skill's frontmatter plus a pointer to the full skill shipped with the package at %_datadir/zoryn/skills/zoryn/SKILL.md — into ~/.claude/skills/zoryn/ (Claude Code, opencode) and ~/.agents/skills/zoryn/ (Codex, opencode). One source of truth that updates with the package; sound because the skill only matters where zoryn is installed (so the referenced file is always present, including inside a devenv). Also agent skill uninstall, agent skill status (where installed / whether the reference resolves), agent skill path and agent skill show. --dir overrides the targets, --force replaces a previous zoryn reference. Install and uninstall only ever touch folders zoryn itself wrote (identified by a marker) — a hand-written skill or unrelated files are refused/left untouched, never deleted.

  • zoryn devenv --debug now dumps every cache-key input (packages, build, mounts, features, prompt, image, branch, spec_hash, …) plus the resulting key and whether it was a cache hit/miss — so two runs can be diffed to see why an environment rebuilt.

  • zoryn devenv --rebuild drops the current environment's container and image and recreates them (podman), honouring all other options (profile, branch, …); reuses the podman build layer cache unless --no-cache is also given. --no-cache is now a pure build modifier (passes podman build --no-cache) that on its own neither forces a rebuild nor deletes anything — combine --rebuild --no-cache for a full from-scratch rebuild.
  • zoryn devenv (podman) no longer rebuilds when the spec changes: the spec is out of the podman cache key, so the existing container is reused and its BuildRequires are reconciled — re-resolved inside the container and the missing ones apt-installed (idempotent). A digest of the spec's BuildRequires lines, recorded inside the container (/var/lib/zoryn-devenv/br_digest), lets an unchanged spec skip re-resolution entirely. The resolved deps are recorded per hasher pass (build_requires_pre, build_requires) and shown by zoryn devenv inspect. Removed BuildRequires are left installed (use clean for a fresh env). bwrap still keys on the spec (chroot rebuilt on change).
  • zoryn devenv now records what each podman container was built from as zoryn.* labels at creation (cache key, image, branch, packages, features, mounts, build steps, spec digest, …); zoryn devenv inspect <CONTAINER> prints them, so environments self-describe and two can be diffed to see which input differs.
  • zoryn devenv remove <CONTAINER> removes one podman dev-environment container directly by name/ID (from zoryn devenv list) together with its backing image (non-force). Refuses non-zoryn containers.
  • zoryn devenv enter <CONTAINER> enters an existing podman dev-environment container directly by name/ID (from zoryn devenv list), starting it if stopped, with no project-config resolution — --root/--user to pick the user, -- CMD to run a command. Refuses non-zoryn containers.
  • zoryn devenv clean --project PATH removes every podman dev environment of the project at PATH (its directory path, as shown by zoryn devenv list, or its basename) from anywhere — without being in that directory and without needing a resolvable config there.
  • zoryn devenv bundled codex feature (installs the OpenAI Codex CLI via its official installer; bind-mounts ~/.codex so its login persists).
  • zoryn devenv bundled CLI features (claude, codex, opencode) now add their install dir to PATH in the dev user's ~/.bashrc (claude/codex → ~/.local/bin, opencode → ~/.opencode/bin), so the installed tool is found in the shell.
  • zoryn devenv feature options can now parameterise mounts via ${NAME} substitution; the bundled vim feature gains a config_access (ro/rw) option controlling whether ~/.vim is mounted read-only (default) or read-write.
  • zoryn devenv bundled vim feature (installs vim-console + vim-plugin-spec_alt-ftplugin and bind-mounts the host's ~/.vimrc read-only and ~/.vim read-write); mount ~/~/ now expands on the container side too (not just the host), and read-only (:ro) mount sources are no longer auto-created when missing.
  • zoryn devenv feature selection is per-layer like every other [devenv] key: when a profile is active only its [devenv.profiles.<p>.features.*] tables apply; the base [devenv.features.*] layer is not inherited into profiles.
  • zoryn devenv mounts now expand a leading ~/~/ (host side) to $HOME; the bundled claude feature mounts ~/.claude and ~/.claude.json so its login persists.
  • zoryn devenv features: select reusable feature.toml bundles via [devenv.features.<id>] tables (devcontainer-style; the table's keys are the feature's typed options). A feature contributes packages + run/run_user commands + mounts (features-first), with options passed as an env prefix. Bundled features are embedded in the binary; a local ~/.config/zoryn/devenv/features/<id>/ overrides them. Bundled: claude, opencode. Podman backend only; machine config only.
  • zoryn devenv config profiles: [devenv.profiles.<name>] is a fully independent [devenv] configuration (changed from the earlier overlay behaviour) — when a profile is active every key is read only from it, the base [devenv] is not inherited or extended; the base applies only when no profile is active. Selected with --profile/-p or [devenv].default_profile. A package's BuildRequires and committed .gear/devenv packages still apply regardless of profile. New [devenv].branch key (default sisyphus) sets the target ALT branch and derives the default podman base image, so e.g. sisyphus and p11 environments coexist and switch by flag. --branch/-B is a one-shot branch override. Machine config only (~/.zoryn, projects.d/).
  • zoryn devenv bwrap backend: [devenv].apt_config passes a host apt config path to hsh --apt-config, so the chroot is populated from chosen repositories (e.g. a non-default branch) instead of the host hasher default. Folded into the bwrap cache key; the podman backend also honours it as the container's sources.list (when no apt_builder/apt_sources override is set), folded into the podman cache key via apt_sources_list.

  • zoryn devenv now resolves a package's BuildRequires: inside the prepared environment in two passes (modelled on hasher's hsh-rebuild): pass 1 installs BuildRequires(pre): (macro-defining packages), pass 2 runs rpmbuild -bE and installs the full expanded set — so rpm macros and %if conditionals are honoured for the target arch, replacing the previous host-side text parse. Also picks up dynamic BuildRequires injected by build-body macros (%pyproject_build, %set_gcc_version, …) via a %{?_buildrequires_build} dump, and resolves capabilities with several providers by passing apt's APT::Install::Virtual options. Both backends; the spec digest is part of the cache key. (rpm-build is now always present in the environment.)

  • zoryn devenv podman backend: configurable shell prompt via [devenv].prompt — a literal bash PS1 baked into the image's user ~/.bashrc (machine config only; changing it rebuilds the image).
  • zoryn devenv podman backend: [devenv].outbound_interface forces the container's outbound traffic out a host interface via a rootless pasta netns (--outbound-if4/--outbound-if6/SO_BINDTODEVICE, with the host default-route interface as the pasta template, so point-to-point/tun/WireGuard interfaces work) instead of the default --network host (machine config only; changing it recreates the container).
  • zoryn devenv podman containers now run with --init (a minimal PID 1 that reaps zombies — without it git's background auto-gc and other build/test subprocesses pile up as defunct processes and exhaust the pid limit) and a configurable --pids-limit via [devenv].pids_limit (default 4096, 0 = unlimited).

  • zoryn devenv — open an ephemeral development shell with a package's build dependencies plus configured extras, with the project directory bind-mounted (bwrap and podman backends).

  • zoryn devenv install PKG... — install extra packages into the current package's prepared environment (preparing it first if missing); --save also appends them to .gear/devenv.
  • zoryn devenv clean — remove the current package's cached dev environment (bwrap: hsh-rmchroot + cache dir; podman: removes the persistent container and the built image). --all (podman) removes every environment of the current project regardless of cache key, sweeping stale containers left by config changes, without touching other projects (matched by a zoryn.project label).
  • zoryn devenv --image IMAGE — override the podman base image (config [devenv].image or this flag); default is registry.altlinux.org/<branch>/alt:latest.
  • zoryn devenv podman backend builds a small derived image (FROM the base, with apt deps + a user matching your host UID/GID + passwordless sudo provisioned at build time), then runs a persistent --userns=keep-id container (one per dependency set) entered via podman exec as you (non-root) — project files stay owned by your host user, and live install runs apt-get as in-container root (podman exec --user root) with no image rebuild. Provisioning at build time (as root, before the keep-id remap) fixes the apt lock/permission errors of the previous keep-id-then-provision approach. With no backend specified it auto-enters whichever backend already has a cached environment.
  • zoryn devenv --user/-u USER — podman only: enter the container as a specific user (mapped to podman exec --user).
  • zoryn devenv accepts shared options before the sub-command, e.g. zoryn devenv --backend podman install vim.
  • [devenv] build — array of shell commands run as image-build RUN steps (podman backend; as root, after dep install, before the user switch). Machine config only (~/.zoryn, projects.d/), not .gear/devenv.
  • [devenv] mounts — array of host paths / host:container[:opts] specs bind-mounted into the podman container via -v at create time. Machine config only (~/.zoryn, projects.d/), not .gear/devenv.
  • zoryn devenv now defaults to the podman backend when podman is installed (falling back to bwrap), when no backend is configured or cached.

0.41.0 - 2026-06-15

Added

  • Config files now warn on unknown sections and misspelled keys (~/.zoryn, .gear/version-up, projects.d/*.toml, builders.d/*.conf, and batch configs ~/.config/zoryn/batch/*.conf).
  • submit: --with search forms (PKG, ^PKG, PKG:) now also accept pkg.git=tag, finding the task that contains that exact subtask instead of matching by package name only.
  • submit: autodetect kernel-image repos (.gear/rules with specsubst: kflavour, spec Name: containing @kflavour@) and create the tag with -s kflavour=<flavour>, fixing gear-create-tag: specsubst variable "kflavour" is not defined. The flavour comes from -k, [specsubst] kflavour in .gear/version-up, or the branch's <flavour>/<dist> prefix (which also defaults -B to <dist>).
  • gen environment: -d now traces the whole decision path — tool availability checks, SSH key lookups, ~/.gnupg permission check, keystore classification (entries found in private-keys-v1.d), every gpg/git command with its exit status, and the chosen key-generation path.

Fixed

  • sandbox: disable commit/tag signing for hooks run in the sandbox. The empty-home tmpfs has no GnuPG keyring, so a signed git commit in a .gear/up.d/ hook failed with "gpg: skipped … No secret key"; zoryn now forces commit.gpgsign=false/tag.gpgsign=false via the GIT_CONFIG_* env override inside the sandbox. No keys are forwarded in — sign on the host instead.
  • build: Builder.build_file now recreates a missing hasher_dir (e.g. a symlink to tmpfs gone after reboot) before building, fixing task rebuild, build --section fallback and batch-pipeline paths that called it without the prior status check.
  • batch: RPM sync to/from the hasher (Builder.run_sync, Builder.sync_rpms_to_hasher) also recreates a missing hasher_dir first — in batch rebuild mode the upload sync runs before any build and silently failed on a dangling symlink, building without the accumulated RPMs.
  • sandbox: hasher and hybrid runners recreate a missing hasher_workdir (e.g. a symlink to tmpfs gone after reboot) before initialising the chroot, instead of failing the hook. The symlink-resolving directory check is now the shared Gau_common.Shell.ensure_local_dir helper, also used by the builder.
  • security: Rpm_spec.update_version now validates the new version against an allowlist (alphanumeric and . - _ + ~, bounded length) before writing it, rejecting an upstream-derived value that contains spaces, control characters or RPM macro triggers — the spec is left untouched on rejection. This wires up the previously-unused Validation.validate_version guard on the up / gen / spec version-bump paths.
  • security: enable the runtime shell-injection safety net in the production binary. Every command, after template expansion and just before it reaches the shell, is scanned for live $(...)/backtick/${...} substitution outside single quotes; a match prints a warning (non-strict — never aborts). Previously this defense-in-depth layer was compiled in but dormant (only the test suite turned it on). The quote-state tracker is backslash-aware, so the nested-SSH '\'' idiom (e.g. remote $(...) in zoryn builder status) is not flagged.
  • build: build tarballs are now removed on every path — normal return, exception, explicit exit, and Ctrl-C — via a new Gau_common.Cleanup process-exit registry (zoryn build, zoryn build --section, TUI mode, task rebuild, auto-fix retry). Previously the trailing Sys.remove ran only on success, leaking the $TMPDIR tarball on any other path. The top-level SIGINT/SIGTERM handlers now exit cleanly so at_exit runs, and the apt-config tmpdir cleanup is registered through the same coordinator.
  • build: forked build children (parallel zoryn build, TUI mode) no longer leak the per-builder apt-config temp directory on the remote builder. Unix._exit skips at_exit, so these children now explicitly wipe the inherited cache and clean up their own apt tmpdirs on every exit path — normal, exception, and SIGINT/SIGTERM — via the shared Builder_apt_upload.init_forked_child/cleanup_forked_child helpers (the same mechanism worker_pool already used).
  • build: create_gear_tarball no longer leaks its temporary tarball when gear --commit fails — the empty/partial temp file is removed before returning the error instead of being left in $TMPDIR.
  • gen pypi2spec / gen opam2spec: sanitize upstream metadata (summary, license, version, URL, VCS, description) before writing it into the generated spec, closing an RPM-macro injection where a hostile PyPI/opam %(…)/%{…} value could execute shell or expand macros when rpm parsed the spec. Single-line header fields also collapse newlines to prevent injecting extra spec directives.
  • gen environment: empty private-keys-v1.d/public-keys.d directories left behind by an interrupted gpg2 migration no longer flip a classic (secring.gpg) keyring to "modern" — only a real *.key file in private-keys-v1.d counts, so key checks go through plain gpg instead of the hanging gpg2/keyboxd (#86). The stuck-lock hint now also suggests removing stale GnuPG lock files.

0.40.0 - 2026-06-09

Added

  • sandbox: [sandbox] specbr in .gear/version-up. With specbr = false the hybrid runner skips the package's BuildRequires — the chroot is initialised bare (hsh --initroot-only) instead of building the src.rpm; the configured packages (git, [sandbox.chroot] packages, [sandbox] packages) are still installed. Defaults to true; supersedes a custom [sandbox.chroot] prepare (warns when ignored).
  • submit: autodetect kernel-module template repos and create per-flavour specsubst tags (-k exposed only in such repos, comma-separated and TAB-completing flavours; default flavours come from the module's existing builds). karch is derived from the arches the kernel flavour is actually built for, so a module is not built where its kernel is absent. --dry-run prints each tag's specsubst headers.
  • submit: universal multi-tag --replace — for kernel-module flavours and batch specsubst submits, each new tag replaces the subtask carrying the same value (adds it if missing), instead of only the first tag.
  • task gitclone: new subcommand that clones the git repository of every subtask of a task into the current directory, one per package, at the exact commit built in the task. Fetches from the task's own gears over git:// (resolving archived DONE tasks automatically); skips delete and srpm subtasks.
  • task mkrepo: new (experimental) command — generates a local merged apt repository overlaying a task's RPMs on the base branch mirror, for testing the task against a complete snapshot. The packages to add and remove come from the task's authoritative plan/bin.list.diff (so additions, version replacements, and deletions — including delete-only tasks — are reflected). The base/ index is built by splicing pkglists (reuse the mirror's pkglist.classic, drop the removed headers, splice in the task's pkglist.task) instead of running genbasedir, so indexing reads only the task's packages and is near-instant regardless of branch size. Writes a single pkglist index — plain pkglist.classic by default, or pkglist.classic.xz with --compressed (not both). --force removes a previously generated repo before rebuilding; --clean removes it and exits. A ready apt config (apt.conf/sources.list/priorities) is written to <workdir>/<task_id>/aptconf/, and the generated release identifies the snapshot (Suite/Label = branch, Description = <branch> (<branch source task> plus task <id>)).
  • up: --reset-to-gear flag — hard-resets the package to its published gears/srpms state before updating, discarding local divergence, uncommitted changes, and untracked files (ignored artifacts kept). Resets the branch planned for the update (current if a known ALT branch, otherwise sisyphus) regardless of the active branch. Closes #81.

Fixed

  • up / spec parsing: the RPM built-in macro %nil now expands to the empty string during version computation. A spec composing its version as %base%sublevel%extra with %define extra_version %nil (e.g. kernel-image) previously yielded a bogus current version like 6.18.35%nil, so zoryn up failed with Cannot find any tags for current version '6.18.35%nil'.

  • build (--section): a short-circuit section build of a specsubst: template package (kernel-image, kernel-modules) no longer feeds rpmbuild a raw template — rpmbuild failed with Invalid symbol '@' (0x40) in: Name: kernel-image-@kflavour@. zoryn now expands @var@ placeholders before syncing the spec into the chroot, mirroring gear (values from git config gear.specsubst.<var>), and errors with a clear hint when a value is unset.

  • up: the upstream remote is now set up from any forge, not just GitHub/GitLab/codeberg/sourcehut. The cascade (.gear/upstream/remotes → spec Vcs: → spec Url:) is an explicit upstream declaration, so the first candidate that git ls-remote can reach (probed with a timeout and GIT_TERMINAL_PROMPT=0) is used — no host allowlist. Self-hosted Gitea/Forgejo/cgit instances (e.g. 0ad's gitea.wildfiregames.com) work now, and an upstream hosted on altlinux.space (a Forgejo) is accepted like any other reachable forge — unblocking packages like kitsune-adw.
  • sandbox: hybrid mode now also bind-mounts the chroot's /var (read-only) into bwrap, alongside /usr, /lib, /lib64, /etc, /bin, /sbin, with a writable /var/tmp tmpfs on top so hooks can use it as scratch.
  • sandbox: the hybrid chroot is now prepared once per run instead of for every hook. Re-initialising per hook was pure overhead — most visible with [sandbox] specbr = false, where the bare init (hsh --initroot-only) rebuilt the chroot from scratch before each script. The bare init also streams its output live (its lines aren't recognised as build stages, so the long basesystem step previously looked like a hang).
  • task mkrepo: a package rebuilt with an identical epoch:version-release is now taken from the task, not the stale base mirror. bin.list.diff omits such rebuilds, so the task's own package set is now authoritative: every task build replaces the same-named base copy in both the symlink farm and the spliced index.
  • gen environment: GPG key generation no longer hangs silently. zoryn now reads the passphrase itself and feeds it to gpg via --batch --passphrase-fd (so gpg never blocks on its own /dev/tty prompt after the migration messages of a legacy ~/.gnupg), pre-checks ~/.gnupg/$GNUPGHOME ownership and permissions and aborts with a fix command when unsafe, and prints recovery hints on failure. It also detects a classic keyring (secret keys in secring.gpg, no keybox) from disk and drives it with gpg instead of gpg2gpg2 would migrate it and can hang on keyboxd's lock. Both listing and generation then go through gpg: a genuine GnuPG 1.4 gpg generates via a batch control file (so the new key lands in the same secring.gpg the user reads), while a 2.x gpg (the common gpg→gpg2 symlink) uses the loopback --quick-generate-key path so it never prompts and re-hangs. The key-listing calls are guarded with timeout -k, so a wedged agent turns into an actionable "run gpgconf --kill all" error instead of an infinite wait. Closes #86.
  • commit / changelog parsing: a changelog version containing letters (e.g. AFLplusplus 5.00c) is no longer dropped. zoryn commit produced a malformed - subject because the header parser only accepted all-numeric versions; it now accepts RPM-legal version characters. Closes #85.
  • up (scheme detection): a merge of an upstream tag (monorepo Merge tag 'hatchling-v1.29.0') is no longer misdetected as tarball-watch. The detector now defers to the tag-based stages — recognizing prefixed v-version tags like <prefix>-v<version> — and names the actual merged tag in its reason.
  • up (scheme detection): a merge of an upstream release tag with a non-semver name (e.g. 0ad's Merge tag 'a27.1') is no longer misdetected as tarball-watch. When the merged commit is absent from every fetched upstream branch, the detector now also matches it by SHA against the upstream remote's own tags (git ls-remote --tags), so any upstream tag-naming convention counts as a real git-merge.
  • builder shell: now mounts the same paths the build uses (/proc, /dev/pts, /dev/kvm, …) and validates/auto-fixes hasher-priv allowed_mountpoints first, so packages whose BuildRequires need /proc work in the interactive shell.
  • submit: --replace now implies --allow-overwrite-tag for the local tag rewrite. Replacing an in-progress subtask after git commit --amend or git rebase no longer aborts with "tag points to commit not an ancestor of HEAD" — the whole point of --replace is to swap in a new commit at the same tag name. A dim notice is printed when the implication is auto-applied. --refresh and the gears-immutability check are unaffected.
  • submit: brand-new packages (not yet in Sisyphus) no longer get stuck on the "tag points to a commit not an ancestor of HEAD" safety check when a stale local tag remains from a previous attempt. When the gears repo is provably missing (ls-remote fails) and the package is unknown to RDB, the local tag is treated as stale and overwritten automatically with a warning. Existing packages keep the strict ancestor check.
  • task test-rebuild: now aborts with an error instead of silently reporting success when the task has no built repository to overlay. Previously a task whose RPMs failed to download, or whose repo was not ready, produced a green run that rebuilt dependents against the clean branch — testing nothing. The download now reports a per-architecture result; an empty listing or any failed RPM aborts the run, as does a run with no available builder architectures. The gate is the task's repository presence, not its state: an early guard only rejects tasks still building (NEW, AWAITING, PENDING, BUILDING, COMMITTING, FAILING), whose RPMS.task would be empty or partial. Terminal states proceed, including FAILED — a failed task often still has a usable (if partial) repository, and test-rebuilding it is a normal workflow; DONE warns the overlay is redundant and FAILED warns the RPM set may be partial. --local-rpms and --without-task are unaffected.
  • submit: --replace with multi-branch -B sisyphus,p11 now finds and replaces the matching task in each branch (one auto-search per branch). Previously the comma-joined repo string was passed as a single filter to task ls, so nothing matched and the command aborted with "no open tasks found". Explicit --replace=TASK_ID is still single-target and errors out when combined with multiple -B repos.

0.39.0 - 2026-05-26

Changed

  • task rebuild: default --dptype is now binary (was source). Binary dependencies match the practical "what breaks if this ABI changes" question better for the rebuild workflow.
  • up (merge-hooks): .gear/merge-up.d/* hooks now run only when this invocation actually produced a merge commit, or when the user explicitly asked for the stage via --stage merge-hooks / --from merge-hooks. Running the full pipeline on an already-up-to-date tree no longer re-runs hooks that already ran in the previous zoryn up (and that had no merge in this run to react to). The persistent pipeline state grew a merge_performed flag (set by stage_merge on success, preserved across --continue); explicit --stage merge-hooks invocations also opt in via a runtime-only explicit_merge_hooks flag that is not persisted, so a re-run never silently inherits yesterday's opt-in.
  • up: new Step 0.3 syncs the local package repo with the public gears/srpms mirror before detection — fast-forwards when behind, auto-merges on divergence, aborts on conflicts or a refused fast-forward. Only runs when the local branch is a known ALT branch (sisyphus, p11, p10, c10f2, …); topic branches like master or wip-* skip silently. Also skipped for packages unknown to RDB, dirty trees, detached HEAD, or fetch failure; fetch is bounded by GIT_HTTP_LOW_SPEED_LIMIT=1000/_TIME=10 so a slow mirror can't hang the run. Closes #80.

Added

  • task copy: --into <task_id> option to append subtasks to an existing target task instead of creating a new one. Target repo is derived from that task (-B ignored); target-task state is validated (DONE/SWEPT/EPERM rejected, in-progress states warned); dependencies are left untouched.
  • task copy: --subtask <SUBTASK> (repeatable, comma-separated) to copy only selected subtasks. Accepts the same syntax as zoryn task delete (numeric ID, package name, pkg.git=tag) with TAB completion from the source task.
  • task test-rebuild: --without-task flag — skips downloading task RPMs and rebuilds dependent packages against the current branch state (baseline). Hasher RPMs are cleared before each individual build to prevent cross-contamination between dependent packages in the same run. Logs get the baseline. prefix. Mutually exclusive with --verify-failures and --local-rpms.
  • task add: --replace flag to replace an existing subtask in a single command. Target subtask is derived from the action's package name (matches both pkg_name and git_repo); ambiguous matches error out asking the user to delete the unwanted subtask manually. Cannot be combined with multi-package del/rebuild. Closes #76.
  • task add: <task_id>^<subtask_id> positional shorthand for --before <subtask_id> — accepts task references in the form printed by task ls / task show.
  • task add: <task_id>/<subtask_id> positional shorthand for --replace targeting an exact subtask (bypasses auto-find by package name).

Fixed

  • submit / task batch: tag-overwrite safety rework (Closes #71)
  • submit recreates a stale local tag automatically when the existing tag is an ancestor of HEAD (fast-forward case) and not yet published in gears. Refuses with an actionable message otherwise. The sisyphus-only Alt_branches.is_root force-tag shortcut is gone — uniform algorithm on every branch.
  • --allow-overwrite-tag flag (on both submit and task batch) bypasses the gears/ancestor checks when you know the stale tag is unrelated to HEAD. --refresh continues to overwrite the local tag unconditionally regardless of this flag — it is only relevant on non-refresh submits.
  • Output distinguishes Created tag: X (fresh), Reusing existing tag: X (already on HEAD), and Recreated tag: X (was at <sha>) (force-overwrite). Previously every successful tag step printed Created tag even when no tag was created.
  • --dry-run now refuses (exit 1) when the gear specfile is missing or lacks Version/Release (previously synthesized ?-? placeholder tags).
  • Same safety algorithm applies to all batch tag paths (simple, first, remaining) and is consistent with non-batch submit.
  • Side-effect: in batch mode with includes_original_first=true, the first tag is now built via apply_batch_format like the rest of the batch. If you relied on the implicit <project>- prefix, add {name} to tag-name in [batch].
  • Simple-submit defers to gear-create-tag default naming when no local tag exists. Previously the fix forced <project>-<version>-<release> style, breaking packages that historically used bare <version>-<release> tags (e.g. libva-intel-media-driver). Safety algorithm still runs when an existing tag is found under either naming style.
  • task copy: dry-run/real run on a NEW source task whose subtasks have not been fetched yet (no srpm/package/pkgname in the API payload) no longer aborts with "No valid subtasks found". resolve_pkg_name now falls back to the git repo basename from dir so build subtasks are recognised before any artifact has been produced.
  • builder: clear_hasher_rpms now also wipes {hasher_dir}/repo/SRPMS.hasher/. sync_rpms_to_hasher does the same only when delete = true (its default, used by zoryn task test-rebuild); zoryn task rebuild calls it with delete:false and keeps the additive-sync semantics. Output of hsh previously accumulated indefinitely across a test-rebuild batch.
  • test-rebuild: fork_single_task child now clears the inherited apt-tmpdir cache, installs SIGINT/SIGTERM handlers that run cleanup_all before _exit, and explicitly cleans up on normal exit — closing the per-task /tmp/.private/<user>/zoryn.XXXXXXXX/ leak both for completed and interrupted runs.
  • up (merge): zoryn up (and --continue) on a branch where the target upstream tag is already merged no longer reports a bogus "Merge failed - conflicts detected". The pipeline now detects that the tag is reachable from HEAD and skips the merge step, letting the remaining stages run.
  • task manage: when running a task that contains rebuild/delete subtasks (or targets a stable repo), gyle's "please specify a reason" rejection no longer ends the action with an "Error: …" status. The TUI now detects gyle's reason-required errors and re-opens the build-reason input prompt prefilled with the existing task message, preserving the original run mode (default / --commit / --test-only) so the retry uses the same flags. Also fixes the previously silent path where pressing r (plain run) on a rebuild task would just fail.
  • gear rules parsing: any tar.<comp>: directive (where <comp> is a non-empty lowercase-alphanumeric suffix) is now recognised by the .gear/rules parser, replacing the previous hand-maintained allowlist of tar.gz: / tar.bz2: / tar.xz:. Concretely, tar.zst: was missed: in a package whose .gear/rules started with e.g. tar.zst: thunderbird followed by a secondary tar: l10n name=l10n, find_tar_directory skipped the first directive and zoryn up tried to update the l10n subdirectory instead of the main source tree. The four duplicated prefix lists (in tar_line_uses_version_template, parse_tar_directory, main_tar_uses_version_template, and rewrite_rules_for_upstream_git) now share a single helper, so future compression suffixes added by gear are picked up automatically rather than producing a silent miss.
  • up (merge-hooks): zoryn up on an already-updated project (or --stage merge-hooks after the fact) no longer feeds the bare spec version into $TAG, which broke hooks doing git rev-parse "$TAG^{}" with ambiguous argument '1.5.0^{}' for upstreams that tag as e.g. v1.5.0 (kryoptic). The stage now resolves the spec version to an actual tag via Gau_git.find_all_matching_tags (honoring .gear/rules tag prefixes and pkg_name- patterns), and skips merge-hooks cleanly when no tag matches instead of running with a non-ref string. Closes #78.
  • task batch: main package no longer fails with gear-create-tag failed (and a follow-up src refspec ... does not match any on push) when re-running for a second target repo: the per-package tag is now resolved up front and reuses the bare <version>-<release> tag on HEAD when the batch-formatted tag is not present, so push and task add reference the tag that actually exists. Fresh first runs also create the main package's tag with the batch-formatted name (gear-create-tag -n …) for consistency.

0.38.0 - 2026-05-05

Added

  • task refresh: --run queues the task for build right after a successful refresh; pair with --test-only / --commit (mutually exclusive) and -m MESSAGE to forward those flags to task run.
  • changelog: web_regex parser for upstream HTML release notes (e.g. Wireshark). Configure parser = "web_regex", url, web-regex-pattern (PCRE with named groups (?<id>...) and (?<desc>...)), and optional web-regex-stop-at substring marker in .gear/version-up. URL-based parsers (web_regex, markdown, osv-json, html-table) also gain {old_version} / {new_version} placeholder substitution. CVE descriptions are RPM-macro-escaped (%%%) before being written into the spec %changelog, so upstream prose containing literal % cannot trigger macro expansion.

Removed

  • config: dropped legacy INI config support — the zoryn_config_fix migration utility, the INI parser, and the on-load auto-detection path are gone. INI-style ~/.zoryn and .gear/version-up now print a TOML parse warning on stderr and fall back to defaults; INI-style batch configs fail with a parse error. Convert them to TOML manually.

Fixed

  • task delete/approve/disapprove/log: pkg.git=tag now picks the subtask whose tag matches (not the first sharing the git repo); a bare ambiguous pkg errors out and lists the candidates. Closes #74.
  • bash completion: TAB after pkg.git=tag prefix no longer stalls or duplicates the pkg.git= prefix in the reply.
  • config: invalid TOML in any user-edited config file now fails with zoryn: <path>:<line>:<col>: invalid TOML: <msg> and exit 1, instead of silently falling back to defaults with only a stderr warning. Previously a typo in .gear/version-up would silently swallow the entire [changelog] section so zoryn up produced an entry without CVE references; ~/.zoryn, builders.d/*.conf, and the [commands] section had the same silent-fallback hazard. Toml_utils.warn_parse_error (the helper that printed the warning) is replaced by fail_parse_error that raises Failure, caught by main.ml's top-level handler. Batch config files (which already returned Error from a result-typed API) keep their result-based behavior; only the redundant stderr warning is dropped there.
  • tests: test_cmd_gen_watch's "fails for nonexistent package" case now runs with ZORYN_TEST_FIXTURES set, so the binary uses fixture-mode HTTP instead of hitting live Debian/Gentoo/Arch endpoints. Without it the test was flaky on CI — a transparent proxy or stray upstream hit could mark a bogus package as "found" and the expected non-zero exit never came.
  • up: scheme detection now falls back to tarball-watch when every structural git-merge signal is absent but a .gear/watch (or a copy: *.watch rule / Source: *.watch) is present. Previously the scheme stayed unknown and zoryn up failed with "no upstream URL found" for packages whose upstream is a plain HTTP tarball site (e.g. dhcp_probe from www.net.princeton.edu).
  • up: for tarball-watch packages whose spec Url: points at PyPI, the version now comes from PyPI and the watch file is consulted only to find the matching tarball download URL. Previously the pipeline searched for a git tag matching the PyPI version (fails without an upstream git remote, e.g. python3-module-grpcio-tools), and the pure-watch fallback was prone to picking upstream pre-release tags like 1.80.0-pre1 over the real release 1.80.0.

0.37.0 - 2026-04-22

Security

  • up (hooks): zoryn up now lists every .gear/{up,merge-up}.d/* script by name before the [y/N] prompt when hooks would run unsandboxed (via --no-sandbox, --sandbox direct, a missing runner binary, or runner=direct in ~/.zoryn), and strips control bytes and Unicode bidi/zero-width/line-separator/BOM hazards from the displayed names (CVE-2021-42574 "Trojan Source" class). The announcement, rejection notices, and prompt all share stdout so stderr redirection cannot silence part of the picture; --continue no longer inherits --no-sandbox or cached consent across processes.

Changed

  • up (hooks) — BREAKING: symlinks under .gear/up.d/ and .gear/merge-up.d/ (and the hook directories themselves if they are symlinks) are now refused: the target may differ from the displayed name, so a .gear/up.d/innocent -> /bin/sh trick would defeat the new list-before-prompt safeguard. Replace such entries with regular files (copy, include, or use a helper script that invokes the real target).

Fixed

  • task approve/disapprove all: delegate the all target to gyle in a single SSH call so stale client-side subtask state no longer yields subtask #N not found cascades before the real subtasks are reached. Also warn when --revoke is combined with -m (gyle ignores the message). Closes #68.
  • task refresh: stop picking stale repo snapshots on girar — get_last_done_task_id sorted candidate tasks by task_id descending and picked the first DONE one, but girar does not commit tasks in task_id order (a task with a higher id can finish before one with a lower id). The reference snapshot could therefore predate already-committed newer versions, so task refresh reported "all packages up to date" even when subtasks were clearly behind repo. Now sort by task_changed descending (with task_id desc as a tiebreaker) to pick the latest-committed DONE task.
  • task manage: no more "ghost symbols" on the subtask-detail and dependencies headers — the colored #task_id / Sub #sub_id -- STATE -- repo -- owner overlay on the subtask-detail screen was drawn one column left of the plain header text (" / Sub " is 7 chars, not 6), so each value's trailing char bled through from the uncolored underlay (Sub#1000, try 11, sisyphuss, nickell). The -- Dependencies of #id overlay was off by one the other way (the prefix is 19, not 20 chars), rendering a doubled ##id. Closes #70.
  • task manage: mouse selection no longer leaks across screen boundaries — clicking on TaskList, then entering the log viewer (or the other way around) used to leave the old (row, col) selection in place; the log view then re-interpreted it as logical (screen_line, col) and painted reverse-video in the wrong spot, and the idle-release copy grabbed whatever happened to live at those coordinates. Selection overlays and the copy path are now gated on the screen the selection was started on.
  • task manage: mouse selection is cleared on every data refresh (manual R/F5, SSE push, auto-poll fallback). The absolute (row, col) coordinates became stale as soon as the task list / detail was reloaded, so the copy-on-release path risked returning text from an unrelated row.
  • task manage: Copied: … preview truncation is UTF-8 safe and no longer slices a multi-byte character mid-sequence (the status line used String.sub on byte offsets).
  • Shell mock layer (tests): SSH commands now require a fixture when ZORYN_TEST_FIXTURES is set — previously the test harness silently fell back to real ssh when the fixture file was missing, so tests could accidentally connect to a live build server against the developer's credentials (and pass or fail depending on their real task state). Missing fixtures now raise a clear Shell fixture missing error. Fall-through behaviour is preserved for git/gear/hsh because many tests rely on real local-only git operations on fixture-built repos.

Added

  • task manage: full mouse text selection on the task list, detail, deps and subtask detail screens — drag to select a range, double-click to select a word. Triple-click on a task list entry's wrapped package block selects all pkg=version items of that task (for other triple-click targets the clicked screen line is selected). Multi-row copies are joined with spaces (no line breaks) so a block of packages lands on a single paste-ready line. Copy fires automatically on mouse release (after ~0.4 s idle) via OSC 52. Single click on a #NNN token still selects just the digits and copies the bare number.
  • docs/site: project website at https://rider.altlinux.team/zoryn/ — a bilingual landing page plus full MkDocs Material documentation (Installation, Quick start, Configuration, Build farm, Hooks, Sandbox, Batch configs, per-command reference). Sources under docs/{en,ru}/; this CHANGELOG.md at the repo root is the single source for the "Changelog" page (symlinked from docs/en/changelog.md; RU falls back to EN via mkdocs-static-i18n).
  • ci: .forgejo/workflows/deploy-site.yaml — builds the MkDocs site and publishes site/ to Forgejo Pages on every push to master touching site/, docs/, mkdocs.yml, overrides/, or dune-project. The landing's version badge is baked from dune-project at build time by overrides/hooks.py — one source of truth for the displayed version. Closes #28.
  • up: new [tarball] subdir config field — subdirectory name inside the tarball to extract (passed as gear-update --subdir=<value>). Supports {version} and {name} placeholders, e.g. subdir = "thunderbird-{version}". Validated against shell metacharacters, path separators (/) and directory-traversal values (., .., or a .. prefix) before use so it cannot escape the intended extraction directory. Useful for tarballs with multiple top-level entries like Mozilla source tarballs
  • up: new parser = "mozilla" in [changelog] — fetches CVEs from mozilla/foundation-security-advisories for Firefox, Firefox ESR, Thunderbird (override with mozilla-product).

0.36.0 - 2026-04-15

Added

  • task manage: show approval/disapproval status in the task list — aggregated / line with usernames between task header and package list
  • Persistent search history in TUI search inputs (test-rebuild, up, task manage). Use Up/Down (or Ctrl+P/Ctrl+N) to walk previous patterns and Ctrl+R for reverse incremental search, readline-style. History is kept in ~/.local/state/zoryn/<tui>.search.history (max 500 entries per TUI). Closes #61.
  • build --section: new bb and ba values (with long-form aliases pkgbinary and pkgall) — run rpmbuild -bb --short-circuit / -ba --short-circuit in the existing chroot to repackage only (Processing files → rpm, or src.rpm + rpm), reusing the %prep/%build/%install artifacts from the previous build. Useful when iterating on %files, packaging macros, or metadata without rerunning the full pipeline through hsh.

Changed

  • test-rebuild log-viewer: pressing Enter on an empty search now re-runs the most recent history entry instead of clearing the match highlight.
  • build (python auto-deps): auto-fix retry now uses hsh-rebuild to reuse the existing chroot instead of a clean hsh build, significantly reducing rebuild time since all BuildRequires are already installed. Falls back to clean build if chroot is unavailable. (#56)

Fixed

  • task rebuild: --skip <pkg> now filters the dependency list in --deps-by-pkg/--from-log/default modes as well (previously silently ignored outside --all-subtasks); numeric subtask IDs in --skip still apply only to --all-subtasks and now emit a warning elsewhere.
  • up: upstream fetch no longer aborts when a local tag would clobber an upstream tag — the fetch is split into a mandatory branches step (--no-tags) and a non-fatal tags step. Errors now name the spec file and the real reason instead of the generic "No upstream remote available".
  • submit: no longer attaches bogus --deps on tasks from a different build chain. Girar.is_repo_fresher used a flat branch_sorting_order index, so p11 (p-chain) was ranked "fresher" than c10f2 (c-chain) and find_upstream_tasks fed those tasks into --deps. It now consults Alt_branches.get_build_predecessors, which understands the real chains sisyphus → p11 → p10 → p9 → p8 and sisyphus → c10f2 → c9f2. For c10f2 only sisyphus is considered upstream; p11/p10 tasks are ignored. (#65)
  • build --section install: drop --nocheck from the rpmbuild arguments. ALT Linux rpmbuild does not accept --nocheck (it's a hsh-level option), so zoryn build --section install failed immediately with --nocheck: unknown option. The flag was also redundant: --short-circuit tells rpmbuild to jump straight to the requested stage, so -bi --short-circuit runs only %install and %check is never reached.
  • build (python auto-deps): auto-fix now detects when upstream has changed the check dependencies source (e.g., switching from setuptools to hatchling) causing a source's deps to become empty in pyproject_deps.json. Previously the build would silently succeed with cached chroot packages but fail on fresh builds. Now zoryn aborts with a clear message asking to update the %pyproject_deps_resync_check_* macros in the spec file. (#62)

0.35.0 - 2026-04-09

Added

  • commit: new zoryn commit command — native replacement for gear-commit, extracts changelog from spec file as commit message; supports --spec, -m, --no-edit, --amend, -a flags. By default opens the message in the same editor git commit uses (resolved via git var GIT_EDITOR). For vim-family editors (vim/vi/nvim/gvim) the prefilled message is piped through stdin and the buffer is renamed via :file, which makes vim mark the buffer as modified — so ZZ, :wq, :x on an unchanged prefilled buffer all commit (unlike opening vim directly on a file, where these commands are no-ops on unchanged content). Save/abort detection is by file existence: :q! aborts, empty buffer aborts ("commit message is empty"), :cq aborts with non-zero exit. For non-vim editors falls back to git commit -F file --edit, which has standard git semantics. Exports GEAR_COMMIT and GEAR_COMMIT_AMEND env vars for git hooks compatibility

Changed

  • task rebuild / batch rebuild: local SRPM lookup is now stricter — only files whose release starts with alt are considered. Previously the legacy Hasher.find_srpm accepted any release suffix, so a directory containing third-party rebuilds (e.g. foo-1.0-1.fc38.src.rpm, foo-1.0-1.mga9.src.rpm, or vendor packages with no alt prefix at all) could be matched by name. After the unification onto Builder.find_srpm_in_path (which shares its matcher with the existing Builder.find_srpm used everywhere else), such files are silently ignored and task rebuild will print "No src.rpm found" for them. If you mirror non-ALT SRPMs into your [sources] srpms_path, keep them in a separate directory; task rebuild only consumes ALT-tagged SRPMs (this matches the behaviour Builder.find_srpm has always used for remote builders). Package names are now also validated through Gau_common.Validation.validate_package_name before lookup, mirroring the rest of the builder API.

Fixed

  • task rebuild / batch rebuild: SRPM lookup no longer matches files whose name shares a suffix with the requested package. Previously the matcher in Hasher.find_srpm used an unanchored regex (Re.eos only, no Re.bos), so a search for qt6-webengine would substring-match inside dqt6-webengine-6.10.2-alt0.dde.1.src.rpm and return the wrong file. The duplicate path-based finder has been removed: both bin/zoryn/cmd_task_rebuild.ml and lib/batch_pipeline/batch_pipeline.ml now use the new Builder.find_srpm_in_path, which shares its fully-anchored matcher (^pkg-…$) with Builder.find_srpm. The "no src.rpm found" warning in task rebuild was also updated to describe the new pattern (%s-{version}-alt{release}.src.rpm) instead of the old permissive one.
  • up: state file location is now resolved via git rev-parse --absolute-git-dir instead of the hardcoded .git/zoryn-up-state.json path, so zoryn up (and the build pipeline it drives) works inside a git worktree. Previously the top-level .git in a worktree is a regular file (a gitdir: pointer), not a directory, so writing the state file failed with ENOTDIR. The new path lives in the per-worktree git directory (.git/worktrees/<name>/zoryn-up-state.json), which is also naturally isolated — two parallel zoryn up runs in two worktrees of the same package no longer collide. (#60)
  • up: scheme detection on packages whose .gear/upstream/remotes exists (or whose spec carries only a Url: to a public forge, no VCS:) no longer requires the maintainer to add the local upstream remote by hand and no longer takes minutes on repos with hundreds of merge commits. stage_detect now resolves the upstream URL from .gear/upstream/remotes → spec Vcs: → spec Url: (with Git_url.normalize + is_github_or_gitlab_url filter) and fetches BEFORE running scheme detection. The remote-discovery cascade is conservative and never destroys maintainer state: (1) it first walks all local remotes and reuses the one whose canonical URL matches the determined upstream URL — regardless of its name, so vendor/tools/phobos/etc. conventions are honored; (2) failing that, it respects an existing upstream remote even when its URL differs from the spec (the maintainer may have pointed it at a mirror or fork deliberately); (3) only when neither match exists does it git remote add upstream <url> from the determined URL. Fetches use --tags without --force, so packaging-side tags whose name coincides with upstream tags are preserved. With upstream fetched, scheme detection runs a new structural fast path: pick the upstream tag matching the current spec version (v<v>, <v>, release-<v>, release_<v>, rel-<v>, <pkg>-<v>), VERIFY it actually came from upstream by checking that its commit is reachable from refs/remotes/<upstream>/* (so a maintainer-side packaging tag with the same name does not poison the result), and run a single git merge-base HEAD <tag> — non-empty result means HEAD shares SHAs with upstream → git-merge; empty means tarball-imported synthetic SHAs → tarball-watch. Tarball imports cannot fake content-hash identity. Falls through to the legacy Tier 1 / Tier 2 cascade when no matching upstream tag exists locally OR when no candidate is verified as upstream. stage_fetch now reuses the same safe helper as a fallback when stage_detect could not determine a URL, and skips the redundant remote-setup step when the URL is already in state.upstream_url. (e.g. microsoft/onnxruntime: scheme detection went from ~30s of slow tag walking to a single merge-base call.)
  • up: Tier 1 (changelog-history scheme detector) now short-circuits on the first upstream-shaped merge instead of eagerly classifying every merge in the version-bump range — for packages with hundreds of merges between two version bumps and no configured upstream remote, this turns an O(N) walk into O(1) on the common case. The "all merges proven non-upstream → Tarball" branch still requires walking the whole range and remains correct.
  • up: scheme detection no longer misclassifies git-merge packages as tarball-watch when the maintainer uses non-standard merge commit messages such as Merge upstream tag 'X' (e.g. oneDNN). The detector now classifies the last update structurally. The primary signal is commit-identity via the upstream remote: a merge is upstream-shaped iff any non-first parent is reachable from refs/remotes/<upstream>/*, where the upstream remote is resolved from .gear/upstream/remotes (proper git-config parsing with [remote "name"] section headers, case-insensitive keys, #/; comments), spec Vcs:/Url:, or a local remote named upstream/vendor. SHA1 identity is cryptographic — tarball imports cannot fake it. When a remote is configured, its negative answer is authoritative (no fall-through to weaker tag heuristics), so packaging-branch merges with coincidental tags cannot be misclassified. When no remote is configured, the detector falls back to tag-based heuristics (tag pointing at merge commit, tag in parent diff). Tier 1 walks the spec's Version:-bump history with --follow --topo-order, verifies ancestry to skip meaningless ranges after rebase, and handles octopus merges. is_github_or_gitlab_url explicitly excludes ALT-internal hosts (*.altlinux.org, *.basealt.ru, etc.) so ALT's gitlab-based infra is never misclassified as upstream. Multi-subproject packages (dmd-style with [remote "tools"] + [remote "phobos"]) are handled correctly by matching gear section names against local remote names.
  • check version-up: spec file lookup now respects spec: directive in .gear/rules. Previously the command only scanned the repository root and printed "No spec file found, use --from for simulation" for packages whose .spec lives inside .gear/ (e.g. python3-module-hypothesis).
  • up: PyPI tag lookup now finds tags whose .gear/version-up pattern carries a decorative prefix (e.g. hypothesis-python-{major:+}.{minor:+}.{patch:+}) without needing an explicit template; [version] filter is still respected. (#57)
  • gen version-up: when run from .gear/ or any subdirectory of a git repository, write .gear/version-up to the repository root instead of creating a nested .gear/.gear/version-up. The -C/--chdir option still defines the working directory explicitly. (#59)
  • builder add: --hasher-dir was ignored in --multi-add mode; now supports {hasher_number} template (e.g., --hasher-dir '/tmp/h_{hasher_number}'). Without placeholder, template is derived automatically with a warning. Priority: --hasher-dir > --based > default ~/hasher_{hasher_number}
  • task add: TAB completion for package names now searches across all branches instead of filtering by the task's target branch — previously e.g. zoryn task add 411104 copy ocaml-l<TAB> (task for p11) hid ocaml-lzma because it does not yet exist in p11
  • gen pypi2spec: emit VCS: tag when the repository URL comes only from home_page (no project_urls.Repository), e.g. fasttext.
  • gen pypi2spec: --url now overrides any URL discovered in PyPI metadata (was: only used as a fallback) and is reflected in the spec's VCS: line.
  • gen pypi2spec: normalise PyPI repository URLs through a shared Gau_common.Git_url helper — handles git+https:/git: prefixes, trailing slashes, and .git suffix uniformly across all code paths.

0.34.0 - 2026-04-06

Added

  • task refresh: --from <branch> flag for cross-branch sync — detect stale subtasks and replace with copy from source branch
  • task refresh: --types <types> flag to control which subtask types are checked (copy, rebuild, build=gear+srpm, ALL)
  • task test-rebuild: warn about packages already FTBFS on beehive before building; add --skip-ftbfs to skip per-architecture build tasks where the package is already FTBFS
  • TAB completion for package names from Repoteka API in clone, check, task, and gen watch commands

Fixed

  • task ls/show: fixed missing package names for delete/copy/rebuild subtasks — used pkgname field from Tasks API instead of absent package

Changed

  • build: unified single-builder and multi-builder batch build code paths — removed ~300 lines of duplicated logic and the first-package special case

0.33.0 - 2026-04-05

Added

  • tui: mouse text selection in log viewer — click+drag to select, y to copy original text (without wrapping) to clipboard via OSC 52 (#52)
  • tui: Space (page down) and b (page up) keys in log viewer — matching standard less keybindings (#50)
  • Syntax highlighting for log output using sublime-syntax engine (#46)
  • build: --section flag to run a specific rpmbuild section (prep, build, install, check) in an existing chroot with automatic spec file sync, --rpmbuild-args for passing extra rpmbuild arguments — for iterative build development and debugging
  • build: configurable log filename template log_filename in [build] config section (~/.zoryn). Supported variables: {builder}, {batch}, {pkgname}. Default: build.{batch}.{builder}.log
  • build, up: --skip-check now supports rpmbuild value to skip %check section via --rpmbuild-args='--without=check'; bare --skip-check without value skips everything (rpmbuild + post-build checks) (#45)
  • gen pypi2spec: %pyproject_runtimedeps_metadata for upstream runtime dependencies (#48)
  • gen pypi2spec: AutoReq: yes, nopython3 to disable python3 autoreq (#48)
  • gen pypi2spec: %global _unpackaged_files_terminate_build 1 in generated spec (#48)

Changed

  • tui: log viewer line wrapping is now character-based instead of word-based — better for logs with long strings without spaces (#54)
  • build: default log filename format: build-log.{builder} -> build.{batch}.{builder}.log
  • build: removed separate .err log files for parallel builds — stderr is now written to the main log
  • gen pypi2spec: spec filename changed from python3-module-<name>.spec to <normalized-pypi-name>.spec (e.g. .gear/hdbscan.spec) (#48)
  • gen pypi2spec: Url: tag now always uses https://pypi.org/project/<name>/ instead of homepage from metadata (#48)
  • gen pypi2spec: removed %doc line from %files — don't package docs and license files (#48)

Fixed

  • tui: last character lost when wrapping long lines in log viewer — byte offset for syntax highlighting was calculated incorrectly due to a missing space at the wrap boundary (#53)
  • build: show SKIP (yellow) instead of FAIL (red) when architecture is excluded in spec file (ExcludeArch) — detected from build log (error: Architecture is excluded: ...) (#49)
  • gen pypi2spec: add %define pypi_name to generated spec and use %{pyproject_distinfo %pypi_name} instead of %pyproject_distinfo %name — fixes incorrect dist-info path matching (#44)

0.32.0 - 2026-03-31

Added

  • submit: automatically squash duplicate release commits — when re-running zoryn submit after changes, if the previous commit has the same version-release subject (e.g. 6.151.10-alt1), amends it instead of creating a duplicate
  • PyPI version source: for Python packages with URL: https://pypi.org/project/<name>/ in spec, zoryn up and zoryn check version now use PyPI as the source of truth for version numbers instead of git tags — prevents updating to yanked (withdrawn) versions; PEP 440 normalization handles version format differences between PyPI and git tags; use --tag to bypass PyPI detection
  • task test-rebuild --top: full TUI rewrite using LTerm — no more screen flickering, syntax-highlighted build logs with search (/, n/N), full-screen log viewer with follow-mode, build log list (l) showing all results, help screen (F1), hotkeys synced with task manage
  • tui_logview: new shared library for log viewer logic (classifier, word wrap, caching, search) — used by both task manage and task test-rebuild
  • zoryn task abort command — abort a running task by ID (with TAB completion for task IDs)
  • zoryn task approve / zoryn task disapprove commands — approve or disapprove task subtasks from CLI (by subtask number, package name, or all), with optional -m message and --revoke
  • Changelog CVE parser: support CVE:YYYY-NNNNN format (used by ISC Kea/BIND) — normalized to standard CVE-YYYY-NNNNN
  • Changelog version header: support Product X.Y.Z (status) released on Date format (ISC ChangeLog)
  • Tests for Io.read_package_list_file (path traversal, comments, blank lines, errors)
  • Tests for Custom_packages.collect (combined --package/--packages-file logic)
  • Tests for List_utils.chunk with size <= 0
  • Test for Stats.create ~custom_packages:[] normalization to None
  • Extract Custom_packages.collect into test_rebuild library for testability
  • task test-rebuild: --package PKG (repeatable) and --packages-file FILE flags to rebuild a custom set of packages instead of the full RDB dependency tree

Changed

  • task manage: log viewer logic extracted to shared tui_logview library (no user-visible change)
  • task test-rebuild --top: event loop migrated from Unix.select to Lwt for LTerm integration

Fixed

  • gen version-up: tags with package name prefix and v-prefixed version (e.g. --tag freeipmi-1-6-17 --to-version v1.6.17) generated wrong pattern with 4 groups instead of recognizing freeipmi- as prefix — now strips v/V prefix before matching
  • task test-rebuild --top: pressing [f] to view failures on the completion screen did nothing — render always showed the completion box ignoring view_mode, and the post-completion input loop never re-rendered
  • task test-rebuild --top: pressing Enter on failures without log files (e.g. "SRPM not found") did nothing — now shows the error message inline with a "press any key" prompt
  • package_spec: is_alt_release now accepts branch releases (alt1.M110P.1) and architecture-specific releases (alt1_e2k1) — previously only digits and dots were allowed after alt, causing release to be merged into version and breaking version comparison
  • task test-rebuild: --continue-no-refresh now updates packages_to_rebuild in stats when package list changes between runs, preventing stale diff comparisons on subsequent --continue
  • task manage: approve/disapprove with message and run-with-commit message failed in TUI — interactive_quiet mode appended `