Repository navigation
release lane robustness: untimed tree-state probe subprocess, releasehash.py's overloaded exit code 2 #627
Description
Activity
Investigated Finding 1 (the untimed tree-state probe) via /ca:debug; scoping this comment to that finding only, leaving Finding 2 (releasehash.py's overloaded exit code 2) untouched.
Confirmed still reproducible on current main:
core/pysrc/_releaselib.py'srelease_tree_status()(thegit status --porcelainprobe used byrun-pre-tag's before/after tree-state assertion) still passes notimeout=, unlike every other internal git probe in this module (which all usetimeout=30-- see lines 871/886/897/907/926/2442/2455/2481/2493/3093/3110/3144).A minimal fix is ready but NOT landed here — it requires a cross-host version advance (
core/pysrc/_releaselib.pyis vendored byte-identically into all three plugins'hooks/_releaselib.py, confirmed viapython tools/sync-core.py --check) plus a decision about how the fix's changelog entry interacts with the rootCHANGELOG.md's existing## [Unreleased]section, which already carries unshipped entries from separate, unrelated work. That decision isn't mine to make from a bug-fix PR, so I'm leaving the fix out of my current PR and recording the details here instead:Fix: wrap the
subprocess.run(...)call inrelease_tree_status()withtimeout=30(matching this module's own established convention), and catchsubprocess.TimeoutExpiredto return a syntheticsubprocess.CompletedProcess(args, 1, "", <actionable message>)instead of letting it raise. This needs NO caller-side change:git status --porcelainexits 0 regardless of dirty/clean state (verified), so any nonzeroprobe.returncodealready routes through the existing "the probe itself failed" handling in both theclean-tree-statusCLI subcommand andrun-pre-tag's_tree_state()(which maps it to exit 8 -- 'the tree-state PROBE itself failed', never exit 6's 'a command mutated the tree').Regression test idiom (worked and verified, exercised against
core/pysrc/_releaselib.pyloaded ascore_releaselibin.github/scripts/test_release_lib.py):hang = subprocess.TimeoutExpired(cmd=["git", "status", "--porcelain"], timeout=30) with mock.patch.object(core_releaselib.subprocess, "run", side_effect=hang): probe = core_releaselib.release_tree_status({}, ".") self.assertNotEqual(probe.returncode, 0) self.assertIn("timed out", probe.stderr.lower())
confirmed red (uncaught
TimeoutExpired) before the fix, green after. Plus a companiontest_a_normal_git_status_is_unaffectedsanity check against the real repo.Whoever picks this up next: land it via its own /ca:release-aware PR that also resolves the Unreleased-section placement question, bumping ca/ca-codex/ca-pi together.
- added 4 commits that reference this issue
on Sep 24, 2026 SUaDtL commented
on Oct 5, 2026 CollaboratorAuthorMore actionsClosing as fixed by #845, with clean-tree guard follow-up in #852. Internal tree-state probes now have a 30-second timeout and return a clean probe failure on TimeoutExpired; operator-declared commands remain untimed. releasehash exit codes distinguish never-confirmed (2), malformed usage (64), and unknown target (65).
Timeout regression coverage injects TimeoutExpired; CLI regressions exercise the exit-code contract. The published 0.15.1 helper carries the fix. This verification was source/history review, not a fresh real hung-process test.
Finding 1: the tree-state probe subprocess call has no timeout
core/pysrc/_releaselib.py, thegit status --porcelainprobe used byrun-pre-tag's before/after tree-state assertion (around theprobe = subprocess.run([git_executable(), "status", "--porcelain", ...], capture_output=True, text=True, cwd=project_root)call), passes notimeout=. Ifgit statushangs — an index lock held by another process, a network-mounted repo, a credential-helper prompt some git config triggers unexpectedly —run-pre-tag(and by extension the whole/ca:releaselane) hangs indefinitely with no diagnostic, rather than failing loudly with an actionable message.This is a repo-wide pattern, not an isolated oversight — none of
_releaselib.py'ssubprocess.runcalls (thegit --exec-pathprobe, the declared pre-tag command dispatch, this tree-state probe) pass a timeout. That consistency is presumably deliberate for the pre-tag command dispatch itself (an operator's declared build/test command may legitimately run long, and an arbitrary timeout would break real releases). The tree-state probe is different: it is this module's owngit statuscall, not operator-declared shell, and has no legitimate reason to run long.Suggested approach
Add a timeout (with a documented rationale for the chosen value) specifically to the internal
git statusprobe, leaving the operator-declared pre-tag command dispatch untimed. Needs a test proving a hung git process degrades to a clean, distinct error rather than a hang — which needs a controllable slow-git fixture, not just atimeout=addition.Finding 2:
releasehash.py's exit code 2 means three different thingscore/pysrc/releasehash.py'smain()returns exit code2for: (a) malformed CLI usage (wrong subcommand/arg count), (b) an unknown release target, and (c) thechecksubcommand'sNEVER_CONFIRMEDstate (pre-tag commands declared but never confirmed viareleasehash.py record <target>— a normal, expected first-run condition, not an error). The module's own CLI docstring documentscheck's exit codes as "0 confirmed, 1 changed, 2 never confirmed, 3/4 declared-file states" without mentioning the same2is reused for the two other, unrelated failure classes.In practice this is low-impact today:
core/surface/skills/release/SKILL.mdstep 6c, the only production caller, treats any nonzero exit fromcheckthe same way (read the commands, thenrecord), and always invokescheckwith a target the row itself already resolved — so the "unknown target" collision is not currently reachable from the one real caller. It is still a latent trap for any future caller (or a manual/scripted invocation) that tries to distinguish these cases by exit code alone.Suggested approach
Give "bad CLI usage" and "unknown target" distinct exit codes from
NEVER_CONFIRMED, and update the module docstring's exit-code table accordingly. Low risk to fix, but touches a documented CLI contract — needs a test per distinguished code.Evidence
Found during the #578/#579 findings-triage cluster; cross-ref #578.