From 7b3368392e74b3e74f45ddec337d124be64060c3 Mon Sep 17 00:00:00 2001 From: Blasius Patrick Date: Thu, 25 Jun 2026 17:43:41 +0700 Subject: [PATCH] fix: sync stderr after command completion to fix CI flake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a __HERMES_STDBUF_N__ marker written to stderr after each user command in the execution frame. Run() spins until the marker appears in stderrBuf before reading the stderr delta. This fixes a race where the END marker on stdout signals completion before the stderr pipe has finished delivering the command's output. On CI (no TTY), bash -i echoes the input frame to stderr, which arrives via a separate pipe — the sync marker ensures captureStderr has flushed everything before Run reads the buffer. Fixes TestRunStderrScopedPerCommand flake on GitHub Actions runners. Signed-off-by: Blasius Patrick --- internal/exec/shell.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/exec/shell.go b/internal/exec/shell.go index 0a87ccc..7bc8655 100644 --- a/internal/exec/shell.go +++ b/internal/exec/shell.go @@ -345,6 +345,20 @@ func (s *Session) Run(ctx context.Context, cmd string) (string, string, int, err return "", "", -1, res.err } + // Wait for the STDBUF marker to appear in stderrBuf. + // This ensures captureStderr has flushed all stderr from the + // command before we read, avoiding a race where stdout signals + // completion before the stderr pipe has finished delivering. + stdbufMarker := fmt.Sprintf("__HERMES_STDBUF_%d__", seq) + for { + s.stderrMu.Lock() + done := strings.Contains(s.stderrBuf.String(), stdbufMarker) + s.stderrMu.Unlock() + if done { + break + } + time.Sleep(time.Millisecond) + } // Read the stderr produced since fromPos. s.stderrMu.Lock() stderrContent := s.stderrBuf.String()[fromPos:] @@ -394,6 +408,9 @@ func (s *Session) buildFrame(seq uint64, userCmd string) string { // deliberately use $? *after* eval so a pipeline like // `false | true` reports 0. b.WriteString("__hermes_ec=$?\n") + // STDBUF marker: written to stderr after the user command so Run + // can wait for stderr to drain before reading the buffer. + fmt.Fprintf(&b, "echo '__HERMES_STDBUF_%d__' >&2\n", seq) fmt.Fprintf(&b, "printf '%%s\\n' '__HERMES_END_%d__'\n", seq) b.WriteString("printf 'EXIT %d\\n' \"$__hermes_ec\"\n") fmt.Fprintf(&b, "printf '%%s%%s%%s\\n' '__HERMES_CWD_%s__' \"$(pwd -P)\" '__HERMES_CWD_%s__'\n", s.id, s.id)