From 4a9588e55170a280331b642d0d2ae8f4a6b73c0b Mon Sep 17 00:00:00 2001 From: saltbo Date: Fri, 31 Jul 2026 11:03:29 -0400 Subject: [PATCH] fix(ci): initialize cloud e2e command errors --- scripts/cloud-e2e-resilience.mjs | 7 +++++++ scripts/cloud-e2e-resilience.test.mjs | 15 ++++++++++++++- scripts/run-cloud-e2e.mjs | 17 +++++++---------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/scripts/cloud-e2e-resilience.mjs b/scripts/cloud-e2e-resilience.mjs index 006fd696..b7abe053 100644 --- a/scripts/cloud-e2e-resilience.mjs +++ b/scripts/cloud-e2e-resilience.mjs @@ -3,6 +3,13 @@ const CLIENT_TRANSPORT_FAILURE = /(?:\b502\b|ERR_(?:FAILED|TUNNEL_CONNECTION_FAI const TUNNEL_CONTEXT_CANCELED = /(?:Incoming request ended abruptly|Request failed)[^\n]*context canceled/i const QUICK_TUNNEL_REQUEST = /trycloudflare\.com/i +export class CloudE2eCommandError extends Error { + constructor(command, commandArgs, code, output) { + super(`${command} ${commandArgs.join(' ')} exited with ${code}`) + this.output = output + } +} + export function isRetryableQuickTunnelFailure({ commandOutput, tunnelOutput }) { if (QUICK_TUNNEL_502.test(commandOutput)) return true return ( diff --git a/scripts/cloud-e2e-resilience.test.mjs b/scripts/cloud-e2e-resilience.test.mjs index 42c952e0..818c7a2b 100644 --- a/scripts/cloud-e2e-resilience.test.mjs +++ b/scripts/cloud-e2e-resilience.test.mjs @@ -1,7 +1,20 @@ import { describe, expect, it } from 'vitest' -import { cloudE2eAttemptCount, isRetryableQuickTunnelFailure } from './cloud-e2e-resilience.mjs' +import { + CloudE2eCommandError, + cloudE2eAttemptCount, + isRetryableQuickTunnelFailure, +} from './cloud-e2e-resilience.mjs' describe('cloud E2E resilience', () => { + it('provides the command error type before the runner executes', () => { + const error = new CloudE2eCommandError('node', ['playwright', 'test'], 1, 'gateway response') + + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(CloudE2eCommandError) + expect(error.message).toBe('node playwright test exited with 1') + expect(error.output).toBe('gateway response') + }) + it('retries a Cloudflare Quick Tunnel gateway page', () => { expect( isRetryableQuickTunnelFailure({ diff --git a/scripts/run-cloud-e2e.mjs b/scripts/run-cloud-e2e.mjs index 5209851c..1fc3d68f 100644 --- a/scripts/run-cloud-e2e.mjs +++ b/scripts/run-cloud-e2e.mjs @@ -2,7 +2,11 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { spawn } from 'node:child_process' import { Resolver } from 'node:dns/promises' import { createRequire } from 'node:module' -import { cloudE2eAttemptCount, isRetryableQuickTunnelFailure } from './cloud-e2e-resilience.mjs' +import { + CloudE2eCommandError, + cloudE2eAttemptCount, + isRetryableQuickTunnelFailure, +} from './cloud-e2e-resilience.mjs' const args = process.argv.slice(2) const require = createRequire(import.meta.url) @@ -49,7 +53,7 @@ for (let attempt = 1; attempt <= maxRunAttempts; attempt += 1) { break } catch (error) { const retryable = - error instanceof CommandError && + error instanceof CloudE2eCommandError && tunnel && isRetryableQuickTunnelFailure({ commandOutput: error.output, @@ -227,13 +231,6 @@ async function waitForPublicTunnelIp(hostname) { throw new Error(`Timed out waiting for public tunnel DNS: ${hostname}`) } -class CommandError extends Error { - constructor(command, commandArgs, code, output) { - super(`${command} ${commandArgs.join(' ')} exited with ${code}`) - this.output = output - } -} - function run(command, commandArgs, env = {}, captureOutput = false) { return new Promise((resolve, reject) => { const child = spawn(command, commandArgs, { @@ -253,7 +250,7 @@ function run(command, commandArgs, env = {}, captureOutput = false) { } child.on('exit', (code) => { if (code === 0) resolve() - else reject(new CommandError(command, commandArgs, code, output)) + else reject(new CloudE2eCommandError(command, commandArgs, code, output)) }) }) }