run_hook (src/hooks/runner.rs) spawns user shell with cmd.status() or cmd.output() and waits, unbounded. Any hook that blocks forever blocks the release forever, holding .git/ferrflow.lock for the duration.
This is not exotic. The ways a hook hangs in CI are all mundane:
- A command that prompts on stdin (
npm login, gpg asking for a passphrase, a git operation that falls through to a credential prompt). stdin is inherited from the release process, so it waits on a terminal that will never answer.
- A hook that curls an endpoint with no
--max-time.
- A
docker build or test suite that deadlocks.
In the non-verbose path (cmd.output()) there is no output at all while this happens — output is buffered until the process exits — so the job looks frozen with no indication of which hook is responsible. The job eventually dies on the CI wall-clock limit, and then the lock issue applies on top.
function hooks in ferrflow.js / ferrflow.ts are reified into node --input-type=module -e "…" commands and go down the same path, so they inherit the problem.
Proposal
- Add a per-hook timeout, configurable at both levels:
workspace.hooks.timeout as a default and a per-hook override, following the shape onFailure already uses. Default to something generous — 10 or 15 minutes — since build hooks are legitimately slow; the point is to bound it, not to be strict.
- On timeout, kill the process group (not just the child — a hook that backgrounds work leaves orphans otherwise) and route through the existing
handle_failure so onFailure: continue still behaves as configured.
- Close stdin on hook processes (
Stdio::null()) unless explicitly opted in. A release hook has no business reading from a terminal, and this alone removes the most common hang.
- Stream output in the non-verbose path, or at minimum print a "still running after Ns" heartbeat, so a slow hook is distinguishable from a hung one.
Tests
- A hook that sleeps past the timeout is killed and reported as a hook failure with the timeout in the message.
onFailure: continue on a timed-out hook continues the release.
- A hook reading stdin gets EOF immediately rather than blocking.
- A hook that spawns a background child leaves no orphan after the timeout kill.
run_hook(src/hooks/runner.rs) spawns user shell withcmd.status()orcmd.output()and waits, unbounded. Any hook that blocks forever blocks the release forever, holding.git/ferrflow.lockfor the duration.This is not exotic. The ways a hook hangs in CI are all mundane:
npm login,gpgasking for a passphrase, agitoperation that falls through to a credential prompt). stdin is inherited from the release process, so it waits on a terminal that will never answer.--max-time.docker buildor test suite that deadlocks.In the non-verbose path (
cmd.output()) there is no output at all while this happens — output is buffered until the process exits — so the job looks frozen with no indication of which hook is responsible. The job eventually dies on the CI wall-clock limit, and then the lock issue applies on top.functionhooks inferrflow.js/ferrflow.tsare reified intonode --input-type=module -e "…"commands and go down the same path, so they inherit the problem.Proposal
workspace.hooks.timeoutas a default and a per-hook override, following the shapeonFailurealready uses. Default to something generous — 10 or 15 minutes — since build hooks are legitimately slow; the point is to bound it, not to be strict.handle_failuresoonFailure: continuestill behaves as configured.Stdio::null()) unless explicitly opted in. A release hook has no business reading from a terminal, and this alone removes the most common hang.Tests
onFailure: continueon a timed-out hook continues the release.