fix(shell): support non-TTY stdin for qovery shell --command#636
Merged
Conversation
`qovery shell --command ...` panicked with "provided file is not a console" when invoked from a non-interactive context (CI, scripted automation, AI agent runners) because pkg/shell.go called console.Current() unconditionally. Detect TTY via golang.org/x/term.IsTerminal. When stdin is not a terminal, behave like `kubectl exec -i`: pipe os.Stdin/os.Stdout straight through, leave TtyWidth/TtyHeight at zero, and on stdin EOF stop reading without cancelling so the websocket loop can drain remote stdout until the server closes. Read goroutines now take io.Reader / io.Writer instead of console.Console.
Follow-up to the non-TTY support: when piped stdin reached EOF the reader just returned, leaving the websocket open with no signal sent to the remote end. Commands that read until EOF (cat < file, sh -s < script.sh, psql < seed.sql, ...) hung indefinitely because the remote process kept waiting for input it would never receive. The remote side runs under AttachParams::interactive_tty(), so the PTY line discipline interprets a literal 0x04 (EOT) as canonical-mode EOF. The shell-agent already uses this exact mechanism to tear down the remote shell when the gRPC stdin stream ends. Doing the same from the CLI on local stdin EOF is symmetric and requires no server-side changes: send EOT, keep the websocket open so remote stdout can drain, exit when the server closes. Pending bracketed-paste bytes are flushed first so we never drop trailing input. All sends respect ctx.Done() to avoid blocking on cancellation.
Contributor
Author
|
Tests done : |
fabienfleureau
approved these changes
May 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix
qovery shell --command ...so it can be invoked from non-interactive contexts (CI, scripted automation, AI agent runners) without panicking, and so piped commands likecat < fileorsh -s < script.shactually finish instead of hanging on stdin.Two related changes on the same branch:
08129c2Skip PTY allocation when stdin is not a TTY.pkg/shell.gopreviously calledcontainerd/console.Current()unconditionally; that function panics withprovided file is not a consolewhenever the controlling terminal isn't available. The fix detects the TTY viagolang.org/x/term.IsTerminal(os.Stdin.Fd()). When interactive, behavior is unchanged. When non-interactive, we pipeos.Stdin/os.Stdoutstraight through (no raw mode, no size detection) and leaveTtyWidth/TtyHeightat zero. The read/write goroutines now takeio.Reader/io.Writerinstead ofconsole.Consoleso the same code paths cover both modes.1986575Propagate stdin EOF to the remote process via EOT. Without this, the non-interactive reader returned silently on EOF and the remote command kept waiting for input. The fix flushes any buffered bracketed-paste bytes, then sends0x04(EOT) through the existingstdInchannel before returning. The remote side (which always runs underAttachParams::interactive_tty()inrust-backend/shell-agent/src/lib/kubernetes.rs:90) translates EOT to EOF via canonical-mode line discipline. Same mechanism the agent itself uses atrust-backend/shell-agent/src/lib/shell.rs:79. The websocket stays open so remote stdout drains until the server closes the session.Why this matters
Three real workflows are unblocked:
qovery shell --command "./run-migration.sh"from a pipelinecat seed.sql | qovery shell --command "psql"Interactive
qovery shellfrom a developer terminal is unchanged.Test plan
gofmt -l pkg/shell.gocleango vet ./pkg/cleango build ./pkg/... ./cmd/...okgo test ./pkg/(24 pass)qovery shellfrom a real terminal still attaches with raw mode and resizeecho "hello" | qovery-cli shell --command cat -- ...outputshelloand exits cleanlyqovery shell --command "./script.sh" < script.shruns to completion and exitsNotes
golang.org/x/termwas already in the module graph as an indirect dep;go mod tidypromoted it to direct and cleaned up three staleqovery-client-gogo.sumentries (collateral cleanup, not behavioral).ExecShelland risks tearing down stdout before it drains. Pure data-plane EOT is symmetric with what the agent does and requires no server-side changes.Fix #632