dhq deploy --wait silently does not wait whenever stdout is not a TTY. It exits 0 as soon as the deployment is queued, so any pipe, CI job, redirect or agent invocation reads success from a deployment that has not run yet — and may still fail.
Verified against v0.21.0. Documented in the agent skill (#40), not fixed.
The two paths
--wait is skipped in JSON mode. WantsJSON() returns JSONMode || !IsTTY (internal/output/envelope.go:51-55) — the --json flag is not the trigger, the missing TTY is. In internal/commands/deploy.go:588-596 the if env.WantsJSON() branch returns after writing the creation JSON, which makes the if wait block at :598 unreachable. --timeout is inert for the same reason.
- A cancelled deployment exits 0.
internal/commands/watch.go:108-111 treats cancelled as a clean terminal state and returns nil. Only failed is non-zero (:107). This one also affects dhq deployments watch, and the internal watch calls in dhq launch and dhq init, which all share watchDeployment.
Either path alone is enough for automation to record a green deploy that never deployed.
Reproduce
# TTY: blocks until the deployment finishes
dhq deploy -p my-project --branch main --wait
# non-TTY: returns immediately, exit 0, deployment still running
dhq deploy -p my-project --branch main --wait | cat
echo $? # 0
Current workaround (what the skill documents)
id=$(dhq deploy -p my-project --branch main --json=identifier | jq -r .identifier)
dhq deployments watch "$id" -p my-project
test "$(dhq deployments show "$id" -p my-project --json=status | jq -r .status)" = completed
The explicit status == "completed" assertion is required — dhq deployments watch alone still exits 0 on cancelled.
Fix options considered
- Honour
--wait in JSON mode — wait, then emit the final deployment JSON, exiting non-zero on failed and cancelled. Best for callers, but it changes the shape of what dhq deploy --wait --json prints (queued → final), so it is a behaviour change for anyone already parsing it.
- Reject the combination outright, the way
--dry-run + --wait already is at internal/commands/deploy.go:396-399. Honest and cheap, but it breaks scripts that pass --wait today believing it works — arguably a feature, since those scripts are already broken silently.
cancelled → non-zero is worth doing either way, and is independent of the choice above.
Context: DHQ-695.
dhq deploy --waitsilently does not wait whenever stdout is not a TTY. It exits0as soon as the deployment is queued, so any pipe, CI job, redirect or agent invocation reads success from a deployment that has not run yet — and may still fail.Verified against
v0.21.0. Documented in the agent skill (#40), not fixed.The two paths
--waitis skipped in JSON mode.WantsJSON()returnsJSONMode || !IsTTY(internal/output/envelope.go:51-55) — the--jsonflag is not the trigger, the missing TTY is. Ininternal/commands/deploy.go:588-596theif env.WantsJSON()branch returns after writing the creation JSON, which makes theif waitblock at:598unreachable.--timeoutis inert for the same reason.internal/commands/watch.go:108-111treatscancelledas a clean terminal state and returnsnil. Onlyfailedis non-zero (:107). This one also affectsdhq deployments watch, and the internal watch calls indhq launchanddhq init, which all sharewatchDeployment.Either path alone is enough for automation to record a green deploy that never deployed.
Reproduce
Current workaround (what the skill documents)
The explicit
status == "completed"assertion is required —dhq deployments watchalone still exits 0 oncancelled.Fix options considered
--waitin JSON mode — wait, then emit the final deployment JSON, exiting non-zero onfailedandcancelled. Best for callers, but it changes the shape of whatdhq deploy --wait --jsonprints (queued → final), so it is a behaviour change for anyone already parsing it.--dry-run+--waitalready is atinternal/commands/deploy.go:396-399. Honest and cheap, but it breaks scripts that pass--waittoday believing it works — arguably a feature, since those scripts are already broken silently.cancelled → non-zerois worth doing either way, and is independent of the choice above.Context: DHQ-695.