Fix Docker executor native steps connection ending on steps proxy stderr

What does this MR do?

It makes omitWriter.Write in the Docker executor report the bytes it was given, so the first thing steps proxy writes to stderr no longer ends the native steps connection. It also keeps the stderr of a proxy that fails in the error the connection ends with, which until now reached the client only through that same bug.

It is three commits:

  1. Move the goroutine in commandExecutor.Connect that splits the steps proxy stream into copyStepsProxyOutput, unchanged, with tests that pin what it does today. No unit test drives Connect, so nothing tested the copy before.
  2. omitWriter.Error returns nil when nothing was written, and the copy adds the stderr text to an error only when there is some, so a failing stream no longer ends with a dangling : (connection reset: ).
  3. Write returns len(p), and the copy closes the pipe with steps proxy: <stderr text> when the stream ends after anything was written to stderr.

Why was this MR needed?

Write declared a named result n and never assigned it, so every write reported 0 bytes. stdcopy.StdCopy returns io.ErrShortWrite for a write that reports less than the frame, so the first stderr frame ended the copy: the pipe the step-runner client reads was closed with short write: <stderr text>, and the stdout frames after it were lost. Fleeting had the same bug in its copy of this writer and fixed it in gitlab-org/fleeting/fleeting!117 (merged).

Fixing Write alone would lose something. I built gitlab-runner-helper from this branch and ran steps proxy against a socket nothing listens on: it writes FATAL: dialing: dial unix ...: connect: no such file or directory to stderr, nothing to stdout, and exits 1. Today that text reaches the client only as part of the short write error. With Write fixed, StdCopy reads to the end of the stream and returns nil, so the text would be dropped: with the fixed Write and the copy's error handling as it is on main, the stderr then end of stream test case ends with no error at all. The third commit keeps it.

Against a running steps serve, steps proxy wrote nothing to stderr in my run, so the defect shows when a proxy writes anything to stderr. I have not reproduced it with a real job.

What's the best way to test this MR?

go test ./executors/docker/internal/omitwriter/ -count=1 -v
go test ./executors/docker/ -run TestCopyStepsProxyOutput -count=1 -v

The cases the third commit adds or changes fail on the second commit, with a byte count of 0 and short write: ..., and pass on the third. I also ran go test ./executors/docker/... ./steps/..., go test -race on the omitwriter package and on TestCopyStepsProxyOutput, go vet on both packages, golangci-lint 2.12.2 from the CI runner-linters:2.12.2-go1.26.5 image on both packages, which reports no issues, and the check-test-directives script.

Test output on the second commit, with the third commit's tests (excerpt)
--- FAIL: TestCopyStepsProxyOutput (0.00s)
    --- FAIL: TestCopyStepsProxyOutput/stderr_between_stdout_frames (0.00s)
            	            	expected: "first|second|"
            	            	actual  : "first|"
            	            	expected: "steps proxy: a warning\n"
            	            	actual  : "short write: a warning\n"
    --- FAIL: TestCopyStepsProxyOutput/stderr_then_end_of_stream (0.00s)
            	            	expected: "steps proxy: dialing: connect: no such file or directory\n"
            	            	actual  : "short write: dialing: connect: no such file or directory\n"
    --- FAIL: TestCopyStepsProxyOutput/source_fails_after_stderr (0.00s)
            	            	expected: "connection reset: a warning\n"
            	            	actual  : "short write: a warning\n"
--- FAIL: Test_OmitWriter (0.00s)
    --- FAIL: Test_OmitWriter/simple (0.00s)
            	            	expected: 5
            	            	actual  : 0
--- FAIL: Test_OmitWriter_IOCopy (0.00s)
`steps proxy` built from this branch
== proxy against a socket nothing listens on
exit=1
stdout bytes: 0
stderr:
FATAL: dialing: dial unix /tmp/proxy-probe/missing.sock: connect: no such file or directory
== proxy against a running steps serve
exit=0
stdout bytes: 0
stderr bytes: 0

The real stderr line also carries ANSI color codes, left out here.

Relation to !7617 (closed)

!7617 (closed) makes the same change to Write and adds a count assertion to Test_OmitWriter. It does not change steps.go, so with it alone the stderr of a proxy that fails is dropped, as described above: with its change applied to this MR's first commit, which moves the copy unchanged, the stderr then end of stream case of TestCopyStepsProxyOutput ends with no error. This MR also reports that stderr when the stream ends, leaves out the dangling : when there is no stderr, and adds tests for the copy.

I was assigned to #40100 on 2026-10-11 and opened this MR under the hackathon rules.

What are the relevant issue numbers?

Closes #40100

Merge request reports

Loading
Loading