Files
cypress/packages/server/lib/cloud/artifacts/upload_artifacts.ts
T
Jennifer Shehane 6bd522bb74 misc: convert server/lib/modes files to ts + add more cli options to Cloud terminal error outputs (#31211)
* chore: convert server/lib/modes files to ts

* update snapshots

* add back require

* fix call to errors.warning

* Add changelog entry.
2025-03-06 12:39:24 -05:00

288 lines
9.5 KiB
TypeScript

import _ from 'lodash'
import Debug from 'debug'
import type ProtocolManager from '../protocol'
import { isProtocolInitializationError } from '@packages/types'
import api from '../api'
import { logUploadManifest, logUploadResults, beginUploadActivityOutput } from '../../util/print-run'
import type { UpdateInstanceArtifactsPayload, ArtifactMetadata, ProtocolMetadata } from '../api'
import * as errors from '../../errors'
import exception from '../exception'
import { IArtifact, ArtifactUploadResult, ArtifactKinds } from './artifact'
import { createScreenshotArtifactBatch } from './screenshot_artifact'
import { createVideoArtifact } from './video_artifact'
import { createProtocolArtifact, composeProtocolErrorReportFromOptions } from './protocol_artifact'
import { HttpError } from '../network/http_error'
import { printProtocolUploadError } from './print_protocol_upload_error'
const debug = Debug('cypress:server:cloud:artifacts')
const removeWhitespaceAndTrim = (str: string) => {
return str.split(/\n/)
.map((line) => {
return line.trim()
})
.join('')
}
const toUploadReportPayload = (acc: {
screenshots: ArtifactMetadata[]
video?: ArtifactMetadata
protocol?: ProtocolMetadata
}, { key, ...report }: ArtifactUploadResult): UpdateInstanceArtifactsPayload => {
const reportWithoutOriginalError = _.omit(report, 'originalError')
if (key === ArtifactKinds.PROTOCOL) {
let { error, errorStack, allErrors } = reportWithoutOriginalError
if (allErrors) {
const messages = allErrors.map((error) => {
return (HttpError.isHttpError(error) && error.responseBody) ?
`${error.message}: ${removeWhitespaceAndTrim(error.responseBody)}` :
error.message
})
error = `Failed to upload Test Replay after ${allErrors.length} attempts. Errors: ${messages.join(', ')}`
errorStack = allErrors.map((error) => error.stack).join('')
} else if (error) {
error = `Failed to upload Test Replay: ${error}`
}
debug('protocol report %O', reportWithoutOriginalError)
return {
...acc,
protocol: {
...reportWithoutOriginalError,
error,
errorStack,
},
}
}
return {
...acc,
[key]: (key === 'screenshots') ? [...acc.screenshots, reportWithoutOriginalError] : reportWithoutOriginalError,
}
}
type UploadArtifactOptions = {
protocolManager?: ProtocolManager
videoUploadUrl?: string
video?: string | null // filepath to the video artifact
screenshots?: {
screenshotId: string
path: string
}[]
screenshotUploadUrls?: {
screenshotId: string
uploadUrl: string
}[]
captureUploadUrl?: string
protocolCaptureMeta: {
url?: string
disabledMessage?: string
}
quiet?: boolean
runId: string
instanceId: string
spec: any
platform: any
projectId: any
}
const extractArtifactsFromOptions = async ({
video, videoUploadUrl, screenshots, screenshotUploadUrls, captureUploadUrl, protocolCaptureMeta, protocolManager,
}: Pick<UploadArtifactOptions,
'video' | 'videoUploadUrl' |
'screenshots' | 'screenshotUploadUrls' |
'captureUploadUrl' | 'protocolManager' | 'protocolCaptureMeta'
>): Promise<IArtifact[]> => {
const artifacts: IArtifact[] = []
if (videoUploadUrl && video) {
try {
artifacts.push(await createVideoArtifact(video, videoUploadUrl))
} catch (e) {
debug('Error creating video artifact: %O', e)
}
}
debug('screenshot metadata: %O', { screenshotUploadUrls, screenshots })
debug('found screenshot filenames: %o', screenshots)
if (screenshots?.length && screenshotUploadUrls?.length) {
const screenshotArtifacts = await createScreenshotArtifactBatch(screenshotUploadUrls, screenshots)
screenshotArtifacts.forEach((screenshot) => {
artifacts.push(screenshot)
})
}
try {
const protocolFilePath = protocolManager?.getArchivePath()
const shouldAddProtocolArtifact = protocolManager && protocolFilePath && captureUploadUrl && !protocolManager.hasFatalError()
debug('should add protocol artifact? %o', {
protocolFilePath,
captureUploadUrl,
protocolManager: !!protocolManager,
fatalError: protocolManager?.hasFatalError(),
shouldAddProtocolArtifact,
})
if (shouldAddProtocolArtifact) {
const protocolArtifact = await createProtocolArtifact(protocolFilePath, captureUploadUrl, protocolManager)
debug(protocolArtifact)
if (protocolArtifact) {
artifacts.push(protocolArtifact)
}
} else if (protocolCaptureMeta.url && !captureUploadUrl) {
const err = new Error('Invalid or missing Test Replay upload URL')
protocolManager?.addFatalError('protocolUploadUrl', err)
}
} catch (e) {
debug('Error creating protocol artifact: %O', e)
}
return artifacts
}
export const uploadArtifacts = async (options: UploadArtifactOptions) => {
const { protocolManager, protocolCaptureMeta, quiet, runId, instanceId, spec, platform, projectId } = options
const priority = {
[ArtifactKinds.VIDEO]: 0,
[ArtifactKinds.SCREENSHOTS]: 1,
[ArtifactKinds.PROTOCOL]: 2,
}
// Checking protocol fatal errors here, because if there is no fatal error
// but protocol is enabled and there is no archive path, we want to detect
// and establish a fatal error
const preArtifactExtractionFatalError = protocolManager?.getFatalError()
/**
* sometimes, protocolManager initializes both without an archive path and without recording an internal
* fatal error. Test Replay initialization should be refactored in order to capture this state more appropriately.
*/
debug({
archivePath: protocolManager?.getArchivePath(),
protocolManager: !!protocolManager,
preArtifactExtractionFatalError,
protocolCaptureMeta,
})
if (protocolManager && (!protocolManager.getArchivePath() && !preArtifactExtractionFatalError && !protocolCaptureMeta.disabledMessage && protocolCaptureMeta.url)) {
protocolManager.addFatalError('UNKNOWN', new Error('Unable to determine Test Replay archive location'))
}
const artifacts = (await extractArtifactsFromOptions(options)).sort((a, b) => {
return priority[a.reportKey] - priority[b.reportKey]
})
let uploadReport: UpdateInstanceArtifactsPayload = { video: undefined, screenshots: [], protocol: undefined }
const postArtifactExtractionFatalError = protocolManager?.getFatalError()
if (postArtifactExtractionFatalError) {
if (postArtifactExtractionFatalError.captureMethod === 'protocolUploadUrl') {
errors.warning('CLOUD_PROTOCOL_CANNOT_UPLOAD_ARTIFACT', postArtifactExtractionFatalError.error)
} else if (isProtocolInitializationError(postArtifactExtractionFatalError)) {
errors.warning('CLOUD_PROTOCOL_INITIALIZATION_FAILURE', postArtifactExtractionFatalError.error)
} else {
errors.warning('CLOUD_PROTOCOL_CAPTURE_FAILURE', postArtifactExtractionFatalError.error)
}
}
if (!quiet) {
debug('logging upload manifest: %O', {
artifacts,
protocolCaptureMeta,
fatalProtocolError: protocolManager?.getFatalError(),
})
logUploadManifest(artifacts, protocolCaptureMeta, protocolManager?.getFatalError())
}
debug('preparing to upload artifacts: %O', artifacts)
let stopUploadActivityOutput: () => void | undefined
if (!quiet && artifacts.length) {
stopUploadActivityOutput = beginUploadActivityOutput()
}
try {
const uploadResults = await Promise.all(artifacts.map((artifact) => artifact.upload())).finally(() => {
if (stopUploadActivityOutput) {
stopUploadActivityOutput()
}
})
if (!quiet && uploadResults.length) {
logUploadResults(uploadResults, protocolManager?.getFatalError())
}
const postUploadProtocolFatalError = protocolManager?.getFatalError()
if (postUploadProtocolFatalError && postUploadProtocolFatalError.captureMethod === 'uploadCaptureArtifact') {
const error = postUploadProtocolFatalError.error
printProtocolUploadError(error)
}
// there is no upload results entry for protocol if we did not attempt to upload protocol due to a fatal error
// during initialization or capture. however, we still want to report this failure with the rest of the upload
// results, so we extract what the upload failure report should be from the options passed to this fn
if (!uploadResults.find((result: ArtifactUploadResult) => {
return result.key === ArtifactKinds.PROTOCOL
}) && postUploadProtocolFatalError) {
debug('composing error report from options')
uploadResults.push(await composeProtocolErrorReportFromOptions(options))
}
uploadReport = uploadResults.reduce(toUploadReportPayload, { video: undefined, screenshots: [], protocol: undefined })
} catch (err) {
debug('primary try/catch failure, ', err.stack)
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS', err)
await exception.create(err)
}
debug('checking for protocol errors', protocolManager?.hasErrors())
if (protocolManager) {
try {
await protocolManager.reportNonFatalErrors({
specName: spec.name,
osName: platform.osName,
projectSlug: projectId,
})
} catch (err) {
debug('Failed to send protocol errors %O', err)
}
}
try {
debug('upload report: %O', uploadReport)
const res = await api.updateInstanceArtifacts({
runId, instanceId,
}, uploadReport)
return res
} catch (err) {
debug('failed updating artifact status %o', {
stack: err.stack,
})
// eslint-disable-next-line no-console
console.log('')
errors.warning('CLOUD_CANNOT_CONFIRM_ARTIFACTS', err)
if (err.statusCode !== 503) {
return exception.create(err)
}
}
}