Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 15.8.3

_Released 1/13/2026 (PENDING)_

**Bugfixes:**

- Fixed an issue where output could be cut off when the process exits in CI environments. Fixed in [#33213](https://github.com/cypress-io/cypress/pull/33213).

## 15.8.2

_Released 01/06/2026_
Expand Down
25 changes: 25 additions & 0 deletions packages/server/lib/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ const debug = Debug('cypress:server:cypress')

type Mode = 'exit' | 'info' | 'interactive' | 'pkg' | 'record' | 'results' | 'run' | 'smokeTest' | 'version' | 'returnPkg' | 'exitWithCode'

/**
* Waits for a writable stream to drain its buffer.
* This is necessary because when stdout/stderr is piped (e.g., in CI environments),
* writes are buffered and process.exit() would terminate before the buffer is flushed.
*/
const waitForStreamDrain = (stream: NodeJS.WriteStream): Promise<void> => {
return new Promise((resolve) => {
if (!stream.isTTY && stream.writableLength > 0) {
debug('waiting for stream to drain, writableLength: %d', stream.writableLength)
stream.once('drain', resolve)
// Safety timeout to prevent hanging indefinitely
setTimeout(resolve, 5000)
} else {
setImmediate(resolve)
}
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream drain event may not fire causing unnecessary delay

Medium Severity

The waitForStreamDrain function waits for the 'drain' event when writableLength > 0. However, Node.js only emits the 'drain' event after a write() call returns false (indicating the buffer was full). If there's buffered data but all writes succeeded (returned true), the 'drain' event will never fire, causing the code to always wait for the 5-second safety timeout. This could add significant delays to CI builds. A more reliable approach would be to poll writableLength until it reaches 0, or use setImmediate to allow the event loop to flush pending writes.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Moumouls Could you address whether this comment is relevant?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not 100% sure about the correct approach here. The issue is very “low-level,” and I’m not an expert in that area. I was helped by Opus on this task, and the code seems logically sound. Does anyone on the Cypress team have expertise in this kind of stuff?

The 5-second timeout at the end of a full spec run doesn’t seem like an issue to me.

However, I’m not sure what the best approach is to ensure things happen ASAP and to verify that all logs are properly flushed.

If I were coding it myself, I’d probably go with a 1–3 second timeout. I believe that should be sufficient, since this feels more like a millisecond or nanosecond-level issue.

}

const exit = async (code = 0) => {
// TODO: we shouldn't have to do this
// but cannot figure out how null is
Expand All @@ -41,6 +59,13 @@ const exit = async (code = 0) => {
debug('telemetry shutdown errored with: ', err)
})

// Wait for stdout/stderr to drain before exiting to prevent truncated output in CI
debug('waiting for stdout/stderr to drain before exit')
await Promise.all([
waitForStreamDrain(process.stdout),
waitForStreamDrain(process.stderr),
])

debug('process.exit', code)

return process.exit(code)
Expand Down