From d1b9b7735eb470f3a3543e591d8f8fcfb89e5c6f Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 28 May 2026 17:45:44 +0000 Subject: [PATCH] fix(comment): don't block on stdin when body is given positionally (#1011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The bug `tea comment "body"` hangs forever in any non-TTY context — CI pipelines, subshells, agent harnesses, scripts — unless the user explicitly redirects stdin with `< /dev/null`. ### Reproduction (released `tea` 0.14.1) ``` # Plain positional body — hangs forever $ tea comment --repo owner/repo 1 "body text" # Heredoc body — hangs forever $ tea comment --repo owner/repo 1 "$(cat <<'EOM' body text EOM )" # Workaround that works (but shouldn't be necessary) $ tea comment --repo owner/repo 1 "body text" < /dev/null ``` Verified on `gitea.com` (server 1.26.0+dev) with `tea 0.14.1`. Both hanging cases hit a 30s timeout in testing; in normal shell use they hang indefinitely. ## Root cause `cmd/comment.go:58-65`: ```go body := strings.Join(ctx.Args().Tail(), " ") if interact.IsStdinPiped() { if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil { return err } else if len(bodyStdin) != 0 { body = strings.Join([]string{body, string(bodyStdin)}, "\n\n") } } ``` `interact.IsStdinPiped()` is implemented as `!term.IsTerminal(os.Stdin)` — true for *any* non-TTY stdin, not just for piped data. When tea enters this branch in a subshell where stdin is open but no producer ever writes to it (the typical case for shell scripts and automation), `io.ReadAll` blocks waiting for an EOF that never arrives. ## Fix Only consume stdin when the user did **not** supply a positional body. If they passed a body via args, that's their content — ignore stdin entirely. ```go if len(body) == 0 && interact.IsStdinPiped() { // ... read stdin ... body = string(bodyStdin) } ``` ## Behavior matrix | Invocation | Before | After | |---|---|---| | `tea comment "body"` (TTY) | works | works | | `tea comment "body"` (non-TTY, no redirect) | **hangs forever** | **works** | | `tea comment "body" < /dev/null` | works | works | | `echo body \| tea comment ` | works | works | | `tea comment "prefix"` + piped stdin | "prefix" + stdin concatenated | positional body wins, stdin ignored | | `tea comment ` (TTY, no body, no pipe) | opens editor | opens editor | The one behavior change is the prefix-plus-stdin case (5th row). I couldn't find anyone relying on that pattern and it wasn't documented; defaulting to "positional body wins" matches the principle of least surprise. ## Verification Confirmed each row against `dinsmoor/tea-testing` issue #1 on `gitea.com` with the patched binary. Previously-hanging invocations now post in <0.5s. Piped-stdin path unchanged. --- This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step. *pull request created by Tyler's lovingly wrangled demon machine <3* --------- Co-authored-by: Lunny Xiao Reviewed-on: https://gitea.com/gitea/tea/pulls/1011 Reviewed-by: Lunny Xiao Co-authored-by: Tyler Co-committed-by: Tyler --- cmd/comment.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/comment.go b/cmd/comment.go index cbd6f8d7..48e63665 100644 --- a/cmd/comment.go +++ b/cmd/comment.go @@ -56,12 +56,16 @@ func runAddComment(requestCtx stdctx.Context, cmd *cli.Command) error { } body := strings.Join(ctx.Args().Tail(), " ") - if interact.IsStdinPiped() { + // Only consume stdin if no positional body was given. interact.IsStdinPiped() + // is true for any non-TTY stdin (CI, subshells, agent harnesses) — not just + // piped data — so reading unconditionally would block forever in those + // contexts when the body is supplied via args. + if len(body) == 0 && interact.IsStdinPiped() { // custom solution until https://github.com/AlecAivazis/survey/issues/328 is fixed if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil { return err } else if len(bodyStdin) != 0 { - body = strings.Join([]string{body, string(bodyStdin)}, "\n\n") + body = string(bodyStdin) } } else if len(body) == 0 { if err := huh.NewForm(