Skip to content

Release 2.1.192 (2026-08-12)

Bug fixes

  • detect Ansible content under a top-level ansible/ directory (#2790) Add "ansible" to the directory signals in _has_ansible_content so a repo that nests all Ansible content under a top-level ansible/ directory (playbooks plus an ansible/roles tree) is recognized and ansible-lint runs. Previously only root-level roles/ and playbooks/ dirs were detected, so the common ansible/ layout silently skipped the ansible-lint stage. Placing ansible first also covers nested ansible/roles and ansible/playbooks layouts, since those imply the top-level dir. Follow-up to #1667. Refs #1906.

  • add bug and docs to complete the canonical kind axis (#2796) vrg-ensure-label --sync omitted the full kind axis: bug leaned on GitHub's default label and docs had to be created ad hoc mid-migration. Add both as canonical registry entries so --sync seeds a complete, consistent set independent of a repo's GitHub defaults. Drop bug from the collision guard (now owned canonically) and bump the sync provisioning count to 21. Refs #1971.

  • derive clone target from the org layout, not CWD (#2797) vrg-github-repo-init derived the clone parent from Path.cwd(), so launching the wizard from a subdirectory of the org tree (e.g. the org .github clone) proposed cloning into that subdirectory instead of beside the org's sibling repos.

step_clone now derives the parent by walking up from CWD to the ancestor directory named after the org (new derive_clone_parent helper), so the clone lands at // regardless of which subdirectory the operator started in. When no ancestor matches the org and no --target-dir was given, it fails loud with actionable guidance rather than silently cloning into an arbitrary CWD.

foreign_repo_refusal now defers to the same derivation: when the org-layout parent is derivable the run is allowed (the clone lands correctly and resume state is already CWD-guarded in run_wizard), so the reported repro of launching from the org .github clone is fixed rather than merely refused.

  • gate batch ready selection on is_frozen so unfrozen worktrees are excluded (#2798) vrg-submit-pr --all (and --select / the checkbox menu) misclassified a worktree reopened via vrg-pr-workflow unfreeze as submittable: read_pr_fields only checked submitted/pr_metadata and never status, so a post-unfreeze implementing tree that retained pr_metadata passed the filter and got swept into the batch plan.

Route read_pr_fields through freeze.is_frozen — the authoritative ready predicate (status == ready and not submitted) — so the submission selectors can never drift from the freeze the rest of the tooling enforces. A tree that kept its metadata but is back in implementing now raises WorkflowError and is bucketed under Not ready.

Add a regression test covering the status=implementing + retained-pr_metadata (post-unfreeze) case.

  • detect orphaned check-run instead of hanging forever (#2800) vrg-pr-await blocked indefinitely when a PR was content-mergeable but its mergeStateStatus stayed BLOCKED because GitHub left a check-run non-terminal after its backing workflow run had already completed (an orphaned check-run). No further event fires for such a check, so waiting for a state transition never returned and pr-watch hung with no diagnostic.

wait_for_settle now also settles with reason "orphaned_check" when the PR is BLOCKED and every still-pending check is orphaned, reusing github.orphaned_check_names (the same detector vrg-finalize-pr uses) rather than re-deriving detection. The API probe runs only after the cheap local predicates (blocked + pending) hold, and the all-pending-are-orphans guard keeps the watch waiting when a genuinely-running app status is still pending. PrState gains merge_state_status; to_output surfaces merge_state_status and orphaned_checks so pr-watch can report the wedge. Ref #2750

  • update stale close-keyword rejection guidance in vrg-commit and vrg-reword (#2801) The auto-close guard's advice to 'Use Ref #N' is stale: issue linkage is no longer expressed in the commit body. It is recorded at PR-submit time via vrg-pr-workflow report-ready, and vrg-submit-pr auto-selects the keyword (Closes for managed tasks, Ref for legacy issues). Commit bodies must therefore carry no linkage keyword.

Reword both rejection messages to explain that linkage is set on the PR at submit time and drop the 'Ref #N' recommendation. The guard logic is unchanged. Extend the vrg-commit and vrg-reword tests to assert the new guidance and that 'Ref #N' no longer appears.

  • block on the CD run the merge triggers, keyed to the merge SHA (#2802)
  • fix(finalize): block on the CD run the merge triggers, keyed to the merge SHA (#2753)

vrg-finalize-pr's cd-check stage no longer glances at the latest CD run on the target branch — which can be a stale or unrelated run — and instead identifies the run its own merge triggered by the merge commit SHA (mergeCommit.oid, stable across merge/squash/rebase), polls for it to register (dispatch latency), watches it to completion, and fails finalize if it concludes non-success, surfacing the run URL.

The cd-check stage is now fail_fast: a non-success post-merge CD run is a hard finalize failure. --no-wait-cd is the conscious opt-out; the wait is bounded by a dispatch timeout and a watch timeout so finalize never hangs. When the repo has no CD workflow, or no run is dispatched for the merge within the dispatch window, finalize does not block. The cleanup-only/batch-tail path (no merge this invocation) waits on the CD run for the current tip of the target branch.

Adds github.merge_commit_sha and records it on FinalizeContext from the merge stage. Updates the finalize reference and releasing guide.

Archaeology (context only): the finalize cd-check was non-blocking from its introduction (issue #303); the blocking CD wait already lives in vrg-release's confirm-publish phase, so no prior blocking wait in finalize was lost.

  • test(finalize): mock git.read_output in test_main_validation_fails so cd-check is env-independent

The cleanup-only CD check calls git.read_output("rev-parse", target_branch) when no merge SHA is recorded. This test omitted the git.read_output mock its sibling tests use, so it leaked to the real git env: it passed locally (the worktree has a develop ref) but failed in the CI PR checkout (detached HEAD, no local develop ref), which took the 'could not resolve' early return and never reached _wait_for_cd_run. Mock read_output to a fixed SHA so the fail_defer assertion is deterministic and environment-independent.

  • make GitHubTransport.write quiet-by-design on stdout (#2805) GitHubTransport.write streamed its relay-ref git push chatter through git.run -> progress.emit, which prints to stdout outside a progress pipeline. That corrupted the stdout contract of any caller emitting structured output, forcing report-ready to wrap the push in contextlib.redirect_stdout(sys.stderr) (7 e2e tests broke without it) and leaving the same footgun for every future caller.

Route the push chatter to stderr inside write() itself, at the transport boundary, so the transport is quiet-by-design and no caller needs the workaround. The redirect is torn down only after git.run joins its output-pump threads, so every emitted line lands on stderr before write returns. Remove the now-redundant caller-side redirect in _push_relay_ref and its unused contextlib import. Add a test asserting write() keeps stdout clean. Ref #2793.

  • resolve named-branch submit path from actual state, not the mere fact a branch was named (#2806) vrg-submit-pr always routed to the push-free relay path, which dead-ended a branch developed in a local worktree but never pushed to origin: _verify_origin_tip failed with 'not on origin'. The workaround was to drop the branch argument and pick it from the interactive list, which uses the local push+open path.

_relay_open now chooses local-vs-relay from actual state. When the named branch has a local worktree carrying a ready-state AND origin lacks the branch, it falls back to the local submit path (push=True: push, then open) and records the submission on the worktree's state file so the scanner shows it in-flight. Otherwise it keeps the relay path (push=False, verify origin tip). The fallback is a live-run concern only; a dry run still previews via the relay path. Adds a regression test for the named-branch, local-worktree-ready, not-on-origin case plus unit coverage for the new helpers.

  • adopt regenerates managed files instead of resuming from init checkpoints (#2807) vrg-github-repo-init --adopt derived its completed-steps from the permanent chore(init): step N - marker commits, so on a previously-init'd repo it skipped every generation step (notably step 5, ci.yml). That contradicts adopt's "overwrite managed files to canonical" contract and blocked the #2720 re-adopt remedy path.

run_wizard now zeroes local_completed when ctx.adopt is set, so every generation step re-runs; checkpoint-based resume is kept only for an interrupted fresh init. Remote state (repo/branch existence) is still honored via _check_remote_steps.

Adds a regression test asserting a repo carrying step-N markers still regenerates ci.yml under --adopt.

  • raise CI-check poll ceiling 180s -> 1800s to stop spurious merge failures (#2810) vrg-finalize-pr's merge stage waits for CI via github.wait_for_checks, which raised a fatal GitHubAPIError when checks were still PENDING at the _POLL_TIMEOUT_SECS deadline. That constant was 180s, far shorter than real CI (3-8 min), so normal PRs spuriously hard-failed the merge and only succeeded on manual retry once CI finished.

Raise the pending-checks ceiling to 1800s (30 min), matching vrg_finalize_pr._CD_WATCH_TIMEOUT_SECS (#2753). The abort-early semantics are unchanged: wait_for_checks still returns as soon as all checks are terminal, still raises OrphanedCheckError when a still-pending check is orphaned (its backing run completed), and only raises the plain timeout at the new 1800s deadline.

Unify the two former copies of _POLL_TIMEOUT_SECS: lib.github is now the single source of truth and lib.release.subprocess imports it, so the finalize and release/update-deps merge waiters can never drift apart again.

Documentation

  • complete the [container].build-command site docs (#2789) Round out the site docs for [container].build-command / build-cache-files so the human-facing reference matches the shipped feature.

  • container-config.md: add a Trust model subsection (broadening vs system-packages: arbitrary shell, root, build-time; reviewed via the vergil.toml diff, no allowlist; fail-closed build; out-of-workspace artifacts; no build-time secrets by default) and an Inspecting the declared command subsection for vrg-container-build-command --script.

  • ci-architecture.md: add a Repo-specific build steps section covering the two-paths/one-declaration model (baked locally, run per test job in CI via vrg-container-build-command --script).
  • cli-tools-overview.md + reference/index.md: document vrg-container-build-command, and close a pre-existing gap by adding vrg-container-system-packages and vrg-container-cache.
  • typescript/overview.md: note baking a non-npm dependency into the dev image, with the NODE_PATH/CommonJS-require vs ESM-import caveat.
  • CLAUDE.md: point the [container] keys line at the container reference.

  • clarify vrg-validate is single-interpreter, not full coverage parity with PR-CI (#2804) Local vrg-validate runs one dev container on a single Python interpreter (currently 3.14), while PR-CI re-runs the --cov-fail-under=100 gate independently per [ci].versions entry (3.12, 3.13, 3.14). Branch coverage can legitimately differ across CPython versions, so 100% locally can still fail a 3.12/3.13 leg in CI. Reframes the overstated 'parity with PR-CI' claim in CLAUDE.md and ci-architecture.md to state this single-interpreter limitation accurately. Docs-only; accepted-and-documented per issue #2791.

Features

  • extend markdownlint coverage to the epics/ directory (#2799) _find_markdown_files now also discovers epics/*/.md, honouring the existing [markdownlint].ignore exclusions, so the durable per-epic design record (spec.md, plan.md, retrospective.md) is linted alongside docs/site/ and README.md. The markdownlint progress line names the added path, and the discovery tests cover the epics/ scope and its ignore handling.

  • exempt epics/ from MD013 in the markdownlint step (#2808) The markdownlint step now runs as two passes: docs/site/ + README.md against the strict bundled config (MD013 line-length enforced), and epics/ against a new prose-relaxed config (configs/markdownlint-prose.yaml) that disables MD013 while keeping every other rule. Epic specs/plans/retrospectives are long-form prose consumed rendered, not raw, so hard-wrapping at 100 columns fights the author for near-zero value. Both passes always run and the step fails if either reports a violation; both honor the existing [markdownlint].ignore paths. Splits _find_markdown_files into _find_strict_markdown_files and _find_prose_markdown_files so the two scopes are distinct. Refs #2803

CI Evidence: All gates passed — full audit bundle available. Download →