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.5.0

_Released 10/21/2025 (PENDING)_

**Features:**

- When the `run` command requires successful negotiation with the Cypress Cloud API and the `--posix-exit-codes` flag is set, Cypress will now exit with code `112` when it cannot determine which spec to run next due to network conditions. These Cloud API negotiations are required when either `--record` or `--parallel` flags are set. Addresses [#32485](https://github.com/cypress-io/cypress/issues/32485). Addressed in [#32635](https://github.com/cypress-io/cypress/pull/32635).

## 15.4.0

_Released 10/7/2025_
Expand Down
2 changes: 2 additions & 0 deletions packages/data-context/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,9 @@ enum ErrorTypeEnum {
CLOUD_CANNOT_CONFIRM_ARTIFACTS
CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE
CLOUD_CANNOT_PROCEED_IN_PARALLEL
CLOUD_CANNOT_PROCEED_IN_PARALLEL_NETWORK
CLOUD_CANNOT_PROCEED_IN_SERIAL
CLOUD_CANNOT_PROCEED_IN_SERIAL_NETWORK
CLOUD_CANNOT_UPLOAD_ARTIFACTS
CLOUD_GRAPHQL_ERROR
CLOUD_INVALID_RUN_REQUEST
Expand Down
30 changes: 30 additions & 0 deletions packages/errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ export const AllCypressErrors = {
ciBuildId: '--ciBuildId',
})}`
},
CLOUD_CANNOT_PROCEED_IN_PARALLEL_NETWORK: (arg1: { flags: any, response: Error }) => {
const message = normalizeNetworkErrorMessage(arg1.response)

return errTemplate`\
We encountered an unexpected error communicating with our servers.

${fmt.highlightSecondary(message)}

Because you passed the ${fmt.flag(`--parallel`)} flag, this run cannot proceed since it requires a valid response from our servers.

${fmt.listFlags(arg1.flags, {
group: '--group',
ciBuildId: '--ciBuildId',
})}`
},
CLOUD_CANNOT_PROCEED_IN_SERIAL: (arg1: { flags: any, response: Error }) => {
const message = normalizeNetworkErrorMessage(arg1.response)

Expand All @@ -188,6 +203,21 @@ export const AllCypressErrors = {
ciBuildId: '--ciBuildId',
})}`
},
CLOUD_CANNOT_PROCEED_IN_SERIAL_NETWORK: (arg1: { flags: any, response: Error }) => {
const message = normalizeNetworkErrorMessage(arg1.response)

return errTemplate`\
We encountered an unexpected error communicating with our servers.

${fmt.highlightSecondary(message)}

Because you passed the ${fmt.flag(`--record`)} flag, this run cannot proceed since it requires a valid response from our servers.

${fmt.listFlags(arg1.flags, {
group: '--group',
ciBuildId: '--ciBuildId',
})}`
},
CLOUD_UNKNOWN_INVALID_REQUEST: (arg1: { flags: any, response: Error }) => {
const message = normalizeNetworkErrorMessage(arg1.response)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
We encountered an unexpected error communicating with our servers.
Copy link
Contributor

Choose a reason for hiding this comment

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

this is so much easier to diff now


Error: fail whale

Because you passed the --parallel flag, this run cannot proceed since it requires a valid response from our servers.

The --group flag you passed was: foo
The --ciBuildId flag you passed was: invalid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
We encountered an unexpected error communicating with our servers.

Error: fail whale

Because you passed the --record flag, this run cannot proceed since it requires a valid response from our servers.

The --group flag you passed was: foo
The --ciBuildId flag you passed was: invalid
22 changes: 22 additions & 0 deletions packages/errors/test/visualSnapshotErrors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ describe('visual error templates', () => {
}],
}
},
CLOUD_CANNOT_PROCEED_IN_PARALLEL_NETWORK: () => {
return {
default: [{
flags: {
ciBuildId: 'invalid',
group: 'foo',
},
response: makeErr(),
}],
}
},
CLOUD_CANNOT_PROCEED_IN_SERIAL: () => {
return {
default: [{
Expand All @@ -247,6 +258,17 @@ describe('visual error templates', () => {
}],
}
},
CLOUD_CANNOT_PROCEED_IN_SERIAL_NETWORK: () => {
return {
default: [{
flags: {
ciBuildId: 'invalid',
group: 'foo',
},
response: makeErr(),
}],
}
},
CLOUD_UNKNOWN_INVALID_REQUEST: () => {
return {
default: [{
Expand Down
16 changes: 10 additions & 6 deletions packages/server/lib/cloud/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const THIRTY_SECONDS = humanInterval('30 seconds')
const SIXTY_SECONDS = humanInterval('60 seconds')
const TWO_MINUTES = humanInterval('2 minutes')

function defaultTimeout () {
return process.env.CYPRESS_INTERNAL_API_TIMEOUT && !isNaN(Number(process.env.CYPRESS_INTERNAL_API_TIMEOUT)) ? Number(process.env.CYPRESS_INTERNAL_API_TIMEOUT) : SIXTY_SECONDS
}

function retryDelays (): number[] {
return process.env.API_RETRY_INTERVALS
? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
Expand Down Expand Up @@ -411,7 +415,7 @@ export default {
url: recordRoutes.runs(),
json: true,
encrypt: preflightResult.encrypt,
timeout: options.timeout ?? SIXTY_SECONDS,
timeout: options.timeout ?? defaultTimeout(),
headers: {
'x-route-version': '4',
'x-cypress-request-attempt': attemptIndex,
Expand Down Expand Up @@ -485,7 +489,7 @@ export default {
url: recordRoutes.instances(runId),
json: true,
encrypt: preflightResult.encrypt,
timeout: timeout ?? SIXTY_SECONDS,
timeout: timeout ?? defaultTimeout(),
headers: {
'x-route-version': '5',
'x-cypress-run-id': runId,
Expand All @@ -505,7 +509,7 @@ export default {
url: recordRoutes.instanceTests(instanceId),
json: true,
encrypt: preflightResult.encrypt,
timeout: timeout ?? SIXTY_SECONDS,
timeout: timeout ?? defaultTimeout(),
headers: {
'x-route-version': '1',
'x-cypress-run-id': runId,
Expand All @@ -523,7 +527,7 @@ export default {
return rp.put({
url: recordRoutes.instanceStdout(options.instanceId),
json: true,
timeout: options.timeout ?? SIXTY_SECONDS,
timeout: options.timeout ?? defaultTimeout(),
body: {
stdout: options.stdout,
},
Expand All @@ -545,7 +549,7 @@ export default {
return rp.put({
url: recordRoutes.instanceArtifacts(options.instanceId),
json: true,
timeout: options.timeout ?? SIXTY_SECONDS,
timeout: options.timeout ?? defaultTimeout(),
body,
headers: {
'x-route-version': '1',
Expand All @@ -564,7 +568,7 @@ export default {
url: recordRoutes.instanceResults(options.instanceId),
json: true,
encrypt: preflightResult.encrypt,
timeout: options.timeout ?? SIXTY_SECONDS,
timeout: options.timeout ?? defaultTimeout(),
headers: {
'x-route-version': '1',
'x-cypress-run-id': options.runId,
Expand Down
141 changes: 78 additions & 63 deletions packages/server/lib/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import argsUtils from './util/args'
import { telemetry } from '@packages/telemetry'
import { getCtx, hasCtx } from '@packages/data-context'
import { warning as errorsWarning } from './errors'

import type { CypressError } from '@packages/errors'
import { toNumber } from 'lodash'
const debug = Debug('cypress:server:cypress')

type Mode = 'exit' | 'info' | 'interactive' | 'pkg' | 'record' | 'results' | 'run' | 'smokeTest' | 'version' | 'returnPkg' | 'exitWithCode'
Expand All @@ -42,6 +43,8 @@ const exit = async (code = 0) => {
debug('telemetry shutdown errored with: ', err)
})

debug('process.exit', code)

return process.exit(code)
}

Expand All @@ -66,18 +69,31 @@ const exit0 = () => {
return exit(0)
}

const exitErr = (err: any) => {
function isCypressError (err: unknown): err is CypressError {
return (err as CypressError).isCypressErr
}

async function exitErr (err: unknown, posixExitCodes?: boolean) {
// log errors to the console
// and potentially raygun
// and exit with 1
debug('exiting with err', err)

return require('./errors').logException(err)
.then(() => {
debug('calling exit 1')
await require('./errors').logException(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason we can just import errors at the top of the file since we are starting to move away from CJS?


return exit(1)
})
if (isCypressError(err)) {
if (
posixExitCodes && (
err.type === 'CLOUD_CANNOT_PROCEED_IN_PARALLEL_NETWORK' ||
err.type === 'CLOUD_CANNOT_PROCEED_IN_SERIAL_NETWORK'
)) {
return exit(112)
}
}

debug('calling exit 1')

return exit(1)
}

export = {
Expand Down Expand Up @@ -151,7 +167,7 @@ export = {
debug('could not parse CLI arguments: %o', argv)

// note - this is promise-returned call
return exitErr(argumentsError)
return exitErr(argumentsError, Boolean(options?.posixExitCodes))
}

debug('from argv %o got options %o', argv, options)
Expand Down Expand Up @@ -206,80 +222,79 @@ export = {
})
},

startInMode (mode: Mode, options: any) {
async startInMode (mode: Mode, options: any) {
debug('starting in mode %s with options %o', mode, options)

switch (mode) {
case 'version':
return require('./modes/pkg')(options)
.get('version')
.then((version: any) => {
return console.log(version) // eslint-disable-line no-console
}).then(exit0)
.catch(exitErr)

case 'info':
return require('./modes/info')(options)
.then(exit0)
.catch(exitErr)

case 'smokeTest':
return this.runElectron(mode, options)
.then((pong: any) => {
if (mode === 'interactive') {
return this.runElectron(mode, options)
}

try {
switch (mode) {
case 'version': {
const version = await require('./modes/pkg')(options).get('version')

// eslint-disable-next-line no-console
console.log(version)
break
}
case 'info': {
await require('./modes/info')(options)
Copy link
Contributor

Choose a reason for hiding this comment

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

can we just hoist this import or await import it? If not I'll likely get to this in the launcher refactor

break
}
case 'smokeTest': {
const pong = await this.runElectron(mode, options)

if (!this.isCurrentlyRunningElectron()) {
return pong
return exit(pong)
} else if (pong !== options.ping) {
return exit(1)
}

if (pong === options.ping) {
return 0
}
break
}
case 'returnPkg': {
const pkg = await require('./modes/pkg')(options)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just import pkg from '@packages/root' at the top of the file since its a bundled package now and just reference it here instead of ./modes/pkg? Would be much simpler


// eslint-disable-next-line no-console
console.log(JSON.stringify(pkg))
break
}
case 'exitWithCode': {
return exit(toNumber(options.exitWithCode))
break
}
case 'run': {
const results = await this.runElectron(mode, options)

return 1
}).then(exit)
.catch(exitErr)

case 'returnPkg':
return require('./modes/pkg')(options)
.then((pkg: any) => {
return console.log(JSON.stringify(pkg)) // eslint-disable-line no-console
}).then(exit0)
.catch(exitErr)

case 'exitWithCode':
return require('./modes/exit')(options)
.then(exit)
.catch(exitErr)

case 'run':
// run headlessly and exit
// with num of totalFailed
return this.runElectron(mode, options)
.then((results: any) => {
if (results.runs) {
const isCanceled = results.runs.filter((run) => run.skippedSpec).length

if (isCanceled) {
// eslint-disable-next-line no-console
console.log(require('chalk').magenta('\n Exiting with non-zero exit code because the run was canceled.'))

return 1
return exit(1)
}
}

debug('results.totalFailed, posix?', results.totalFailed, options.posixExitCodes)

if (options.posixExitCodes) {
return results.totalFailed ? 1 : 0
return exit(results.totalFailed ? 1 : 0)
}

return results.totalFailed
})
.then(exit)
.catch(exitErr)

case 'interactive':
return this.runElectron(mode, options)

default:
throw new Error(`Cannot start. Invalid mode: '${mode}'`)
return exit(results.totalFailed ?? 0)
}
default: {
throw new Error(`Cannot start. Invalid mode: '${mode}'`)
}
}
} catch (err) {
return exitErr(err, options.posixExitCodes)
}
debug('end of startInMode, exit 0')

return exit(0)
},
}
8 changes: 0 additions & 8 deletions packages/server/lib/modes/exit.ts

This file was deleted.

Loading
Loading