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
1 change: 1 addition & 0 deletions packages/server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib/validations
28 changes: 6 additions & 22 deletions packages/server/lib/cloud/api/create_instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,13 @@ import { asyncRetry, exponentialBackoff } from '../../util/async_retry'
import * as errors from '../../errors'
import { isAxiosError } from 'axios'

const MAX_RETRIES = 3

export interface CreateInstanceResponse {
spec: string | null
instanceId: string | null
claimedInstances: number
estimatedWallClockDuration: number | null
totalInstances: number
}
// Import cloud validation types for better type safety
import type {
PostRunInstanceRequest_v2Type as CreateInstanceRequestBody,
PostRunInstanceResponse_v2 as CreateInstanceResponse,
} from '../../validations/cloudValidations'

export interface CreateInstanceRequestBody {
spec: string | null
groupId: string
machineId: string
platform: {
browserName: string
browserVersion: string
osCpus: any[]
osMemory: Record<string, any> | null
osName: string
osVersion: string
}
}
const MAX_RETRIES = 3

export const createInstance = async (runId: string, instanceData: CreateInstanceRequestBody, timeout: number = 0): Promise<CreateInstanceResponse> => {
let attemptNumber = 0
Expand Down
93 changes: 36 additions & 57 deletions packages/server/lib/cloud/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,26 @@ import type { ProjectBase } from '../../project-base'

import { PUBLIC_KEY_VERSION } from '../constants'

// axios implementation disabled until proxy issues can be diagnosed/fixed
// TODO: https://github.com/cypress-io/cypress/issues/31490
//import { createInstance } from './create_instance'
import type { CreateInstanceRequestBody, CreateInstanceResponse } from './create_instance'

import { transformError } from './axios_middleware/transform_error'
import { DecryptionError } from './cloud_request_errors'
import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes'

// Import cloud validation types for better type safety
import type {
PostRunRequest_v3Type as CreateRunRequestType,
PostRunResponse_v3Type as CreateRunResponseType,
PostRunInstanceRequest_v2Type as CreateInstanceRequestType,
PostRunInstanceResponse_v2 as CreateInstanceResponse,
PostInstanceResultsRequest_v1Type as PostInstanceResultsRequestType,
PostInstanceResultsResponse_v1Type as PostInstanceResultsResponseType,
PostInstanceTestsResponse_v1Type as PostInstanceTestsResponseType,
PutInstanceResponse_v2Type as UpdateInstanceStdoutResponseType,
PutInstanceStdoutRequest_v1Type as UpdateInstanceStdoutRequestType,
} from '../../validations/cloudValidations'

// Define response type for putInstanceArtifacts (returns z.ZodAny with resExample: {})
type PutInstanceArtifactsResponseType = any

const THIRTY_SECONDS = humanInterval('30 seconds')
const SIXTY_SECONDS = humanInterval('60 seconds')
const TWO_MINUTES = humanInterval('2 minutes')
Expand Down Expand Up @@ -249,47 +260,15 @@ function noProxyPreflightTimeout (): number {
}
}

export type CreateRunOptions = {
// Use cloud validation types for better type safety
export type CreateRunOptions = CreateRunRequestType & {
projectRoot: string
ci: {
params: string
provider: string
}
ciBuildId: string
projectId: string
recordKey: string
commit: string
specs: string[]
group: string
platform: string
parallel: boolean
specPattern: string[]
tags: string[]
testingType: 'e2e' | 'component'
timeout?: number
project: ProjectBase
autoCancelAfterFailures?: number | undefined
timeout?: number
}

type CreateRunResponse = {
groupId: string
machineId: string
runId: string
tags: string[] | null
runUrl: string
warnings: (Record<string, unknown> & {
code: string
message: string
name: string
})[]
captureProtocolUrl?: string | undefined
capture?: {
url?: string
tags: string[] | null
mountVersion?: number
disabledMessage?: string
} | undefined
}
// Use cloud validation types for better type safety
type CreateRunResponse = CreateRunResponseType

export type ArtifactMetadata = {
url: string
Expand Down Expand Up @@ -348,26 +327,26 @@ export default {
rp,

// For internal testing
setPreflightResult (toSet) {
setPreflightResult (toSet: any): void {
preflightResult = {
...preflightResult,
...toSet,
}
},

resetPreflightResult () {
resetPreflightResult (): void {
recordRoutes = apiRoutes
preflightResult = {
encrypt: true,
}
},

ping () {
ping (): Bluebird<any> {
return rp.get(apiRoutes.ping())
.catch(tagError)
},

getAuthUrls () {
getAuthUrls (): Bluebird<any> {
return rp.get({
url: apiRoutes.auth(),
json: true,
Expand Down Expand Up @@ -453,7 +432,7 @@ export default {
}
}

if (script) {
if (script && (options.testingType === 'e2e' || options.testingType === 'component')) {
const config = options.project.getConfig()

await options.project.protocolManager.prepareAndSetupProtocol(script, {
Expand All @@ -478,7 +457,7 @@ export default {
.catch(tagError)
},

createInstance (runId: string, body: CreateInstanceRequestBody, timeout?: number): Bluebird<CreateInstanceResponse> {
createInstance (runId: string, body: CreateInstanceRequestType, timeout?: number): Bluebird<CreateInstanceResponse> {
return retryWithBackoff((attemptIndex) => {
return rp.post({
body,
Expand All @@ -497,7 +476,7 @@ export default {
}) as Bluebird<CreateInstanceResponse>
},

postInstanceTests (options) {
postInstanceTests (options: { instanceId: string, runId: string, timeout?: number, [key: string]: any }): Bluebird<PostInstanceTestsResponseType> {
const { instanceId, runId, timeout, ...body } = options

return retryWithBackoff((attemptIndex) => {
Expand All @@ -518,7 +497,7 @@ export default {
})
},

updateInstanceStdout (options) {
updateInstanceStdout (options: UpdateInstanceStdoutRequestType & { instanceId: string, runId: string, timeout?: number }): Bluebird<UpdateInstanceStdoutResponseType> {
return retryWithBackoff((attemptIndex) => {
return rp.put({
url: recordRoutes.instanceStdout(options.instanceId),
Expand All @@ -538,7 +517,7 @@ export default {
})
},

updateInstanceArtifacts (options: UpdateInstanceArtifactsOptions, body: UpdateInstanceArtifactsPayload) {
updateInstanceArtifacts (options: UpdateInstanceArtifactsOptions, body: UpdateInstanceArtifactsPayload): Bluebird<PutInstanceArtifactsResponseType> {
debug('PUT %s %o', recordRoutes.instanceArtifacts(options.instanceId), body)

return retryWithBackoff((attemptIndex) => {
Expand All @@ -558,7 +537,7 @@ export default {
})
},

postInstanceResults (options) {
postInstanceResults (options: PostInstanceResultsRequestType & { instanceId: string, runId: string, timeout?: number }): Bluebird<PostInstanceResultsResponseType> {
return retryWithBackoff((attemptIndex) => {
return rp.post({
url: recordRoutes.instanceResults(options.instanceId),
Expand All @@ -585,7 +564,7 @@ export default {
})
},

createCrashReport (body, authToken, timeout = 3000) {
createCrashReport (body: any, authToken: string, timeout = 3000): Bluebird<any> {
return rp.post({
url: apiRoutes.exceptions(),
json: true,
Expand All @@ -598,7 +577,7 @@ export default {
.catch(tagError)
},

postLogout (authToken) {
postLogout (authToken: string): Bluebird<any> {
return Bluebird.join(
this.getAuthUrls(),
machineId.machineId(),
Expand All @@ -619,11 +598,11 @@ export default {
)
},

clearCache () {
clearCache (): void {
responseCache = {}
},

sendPreflight (preflightInfo) {
sendPreflight (preflightInfo: any): Bluebird<any> {
return retryWithBackoff(async (attemptIndex) => {
const { projectRoot, timeout, ...preflightRequestBody } = preflightInfo

Expand Down Expand Up @@ -680,7 +659,7 @@ export default {
return result
})
},

async getCaptureProtocolScript (url: string, options: { displayRetryErrors?: boolean } = { displayRetryErrors: true }) {
// TODO(protocol): Ensure this is removed in production
if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
Expand Down
79 changes: 36 additions & 43 deletions packages/server/lib/modes/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ async function createInstance (options: InstanceOptions) {
}
}

const _postInstanceTests = ({
async function _postInstanceTests ({
runId,
instanceId,
config,
Expand All @@ -541,17 +541,18 @@ const _postInstanceTests = ({
parallel,
ciBuildId,
group,
}) => {
return api.postInstanceTests({
runId,
instanceId,
config,
tests,
hooks,
})
.catch((err: any) => {
throwCloudCannotProceed({ parallel, ciBuildId, group, err })
})
}) {
try {
return await api.postInstanceTests({
runId,
instanceId,
config,
tests,
hooks,
})
} catch (err: unknown) {
throw cloudCannotProceedErr({ parallel, ciBuildId, group, err })
}
}

const createRunAndRecordSpecs = (options: any = {}) => {
Expand Down Expand Up @@ -778,42 +779,34 @@ const createRunAndRecordSpecs = (options: any = {}) => {
})
.value()

const responseDidFail = {}
const response = await _postInstanceTests({
runId,
instanceId,
config: resolvedRuntimeConfig,
tests,
hooks,
parallel,
ciBuildId,
group,
})
.catch((err: any) => {
onError(err)

return responseDidFail
Copy link
Contributor

Choose a reason for hiding this comment

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

do we no longer need this logic?

})

if (response === responseDidFail) {
debug('`responseDidFail` equals `response`, allowing browser to hang until it is killed: Response %o', { responseDidFail })
try {
const response = await _postInstanceTests({
runId,
instanceId,
config: resolvedRuntimeConfig,
tests,
hooks,
parallel,
ciBuildId,
group,
})

// dont call the cb, let the browser hang until it's killed
return
}
if (_.some(response.actions, { type: 'SPEC', action: 'SKIP' })) {
errorsWarning('CLOUD_CANCEL_SKIPPED_SPEC')

if (_.some(response.actions, { type: 'SPEC', action: 'SKIP' })) {
errorsWarning('CLOUD_CANCEL_SKIPPED_SPEC')
// set a property on the response so the browser runner
// knows not to start executing tests
project.emit('end', { skippedSpec: true, stats: {} })

// set a property on the response so the browser runner
// knows not to start executing tests
project.emit('end', { skippedSpec: true, stats: {} })
// dont call the cb, let the browser hang until it's killed
return
}

// dont call the cb, let the browser hang until it's killed
return
return cb(response)
} catch (err: unknown) {
onError(err)
debug('postInstanceTests failed, allowing browser to hang until it is killed: Error %o', { err })
Copy link

Choose a reason for hiding this comment

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

Bug: Error Handling Skipped After Refactor

The refactor removed the onError(err) callback invocation when _postInstanceTests fails. Errors are now only logged, skipping the onError call that previously handled error reporting or cleanup logic.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Bug: Error Handling Fails to Return Early After API Failure

The refactored error handling for _postInstanceTests in onTestsReceived is missing an explicit return statement in its catch block. This causes the function to continue executing after an API failure instead of returning early, which alters the intended behavior of letting the browser hang.

Fix in Cursor Fix in Web

}

return cb(response)
})

return runAllSpecs({
Expand Down
13 changes: 8 additions & 5 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"private": true,
"main": "index.js",
"scripts": {
"build-prod": "tsc || echo 'built, with type errors'",
"check-ts": "tsc --noEmit",
"build-prod": "yarn ensure-cloud-validations && tsc || echo 'built, with type errors'",
"check-ts": "yarn ensure-cloud-validations && tsc --noEmit",
"clean-deps": "rimraf node_modules",
"codecov": "codecov",
"dev": "node index.js",
"docker": "cd ../.. && WORKING_DIR=/packages/server ./scripts/run-docker-local.sh",
"postinstall": "patch-package",
"postinstall": "patch-package && yarn sync-cloud-validations",
Copy link
Contributor

Choose a reason for hiding this comment

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

if we are doing this at the package level, do we still need to call it in CI after the build? Would be nice to get rid of this https://github.com/cypress-io/cypress/blob/develop/.circleci/src/pipeline/@pipeline.yml#L201

"lint": "eslint",
"rebuild-better-sqlite3": "electron-rebuild -f -o better-sqlite3",
"repl": "node repl.js",
Expand All @@ -19,7 +19,9 @@
"test-integration": "node ./test/scripts/run.js --glob-in-dir=test/integration",
"test-performance": "node ./test/scripts/run.js --glob-in-dir=test/performance",
"test-unit": "node ./test/scripts/run.js --glob-in-dir=test/unit",
"test-watch": "./test/scripts/watch test"
"test-watch": "./test/scripts/watch test",
"sync-cloud-validations": "./scripts/sync-cloud-validations.sh sync",
"ensure-cloud-validations": "./scripts/sync-cloud-validations.sh"
},
"dependencies": {
"@babel/parser": "7.28.0",
Expand Down Expand Up @@ -211,7 +213,8 @@
"tsconfig-paths": "3.10.1",
"webpack": "^5.88.2",
"ws": "5.2.4",
"xvfb-maybe": "0.2.1"
"xvfb-maybe": "0.2.1",
"zod": "^3.23.8"
},
"files": [
"config",
Expand Down
Loading
Loading