diff --git a/src/index.ts b/src/index.ts index 127e231..31848a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -147,9 +147,17 @@ const run = async (): Promise => { let testOutput = ''; let uploadId: string | null = null; + // The CLI's exit code is the primary verdict: it is computed by the process + // that actually watched the run. 0 = every test passed, 1 = CLI/infra error, + // 2 = the run itself failed (a failed test, or a cancelled one). This used to + // be discarded for anything but 1, leaving the separate `dcd status` call as + // the only gate. + let cloudExitCode = 0; + try { const { output, exitCode } = await executeCommand(cloudCommand); testOutput = output; + cloudExitCode = exitCode; if (exitCode === 1) { throw new Error( @@ -185,7 +193,18 @@ const run = async (): Promise => { })); setOutput('flow_results', JSON.stringify(flowResults)); - if (result.status === 'PASSED') { + // Fail on either signal. The exit code is authoritative for a run that + // finished badly; the status call can only add failures the CLI could not + // see. A non-terminal status (PENDING/RUNNING) alongside a clean exit is a + // racy or degraded status call, not a failure — the CLI watched the run to + // completion, so warn rather than fail the job. + if (cloudExitCode !== 0) { + console.error( + `Test run failed (dcd exited ${cloudExitCode}, status ${result.status}). ` + + `Check flow results: ${result.consoleUrl}` + ); + process.exit(1); + } else if (result.status === 'PASSED') { console.error('Successfully completed test run.'); process.exit(0); } else if (result.status === 'FAILED' || result.status === 'CANCELLED') { @@ -194,7 +213,10 @@ const run = async (): Promise => { ); process.exit(1); } else { - console.error(`Test run finished with status: ${result.status}`); + console.error( + `dcd reported success but the upload status is ${result.status}. ` + + `Treating the run as passed: ${result.consoleUrl}` + ); process.exit(0); } } else {