Skip to content

Commit 64df1dc

Browse files
committed
🐛 Make finalize command resilient to missing builds
The finalize command now handles 404 errors gracefully instead of failing CI. When no build exists for a parallel ID (e.g., tests were skipped or no screenshots uploaded), it warns and exits 0. Added global --strict flag for users who want hard failures on any error. This flag is available on all commands. Closes VIZ-108
1 parent eeda9fb commit 64df1dc

3 files changed

Lines changed: 143 additions & 1 deletion

File tree

src/cli.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ program
236236
.option('--json', 'Machine-readable output')
237237
.option('--color', 'Force colored output (even in non-TTY)')
238238
.option('--no-color', 'Disable colored output')
239+
.option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)')
239240
.configureHelp({ formatHelp });
240241

241242
// Load plugins before defining commands

src/commands/finalize.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,42 @@ export async function finalizeCommand(
9696
} catch (error) {
9797
output.stopSpinner();
9898

99-
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
10099
let status = error.context?.status;
100+
101+
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
101102
if (status >= 500) {
102103
output.warn('Vizzly API unavailable - finalize skipped.');
103104
return { success: true, result: { skipped: true } };
104105
}
105106

107+
// Handle missing builds gracefully (404 errors)
108+
// This happens when: no screenshots were uploaded, tests were skipped, or parallel-id doesn't exist
109+
if (status === 404) {
110+
let isStrict = globalOptions.strict;
111+
112+
if (isStrict) {
113+
output.error(`No build found for parallel ID: ${parallelId}`);
114+
output.blank();
115+
output.info('This can happen when:');
116+
output.info(' • No screenshots were uploaded with this parallel-id');
117+
output.info(' • Tests were skipped or failed before capturing screenshots');
118+
output.info(' • The parallel-id does not match what was used during test runs');
119+
exit(1);
120+
return { success: false, reason: 'no-build-found', error };
121+
}
122+
123+
// Non-strict mode: warn but don't fail CI
124+
output.warn(`No build found for parallel ID: ${parallelId} - finalize skipped.`);
125+
if (globalOptions.verbose) {
126+
output.info('Possible reasons:');
127+
output.info(' • No screenshots were uploaded with this parallel-id');
128+
output.info(' • Tests were skipped or failed before capturing screenshots');
129+
output.info(' • The parallel-id does not match what was used during test runs');
130+
output.info('Use --strict flag to fail CI when no build is found.');
131+
}
132+
return { success: true, result: { skipped: true, reason: 'no-build-found' } };
133+
}
134+
106135
output.error('Failed to finalize parallel build', error);
107136
exit(1);
108137
return { success: false, error };

tests/commands/finalize.test.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,5 +339,117 @@ describe('commands/finalize', () => {
339339
)
340340
);
341341
});
342+
343+
it('does not fail CI when no build found (404) in non-strict mode', async () => {
344+
let output = createMockOutput();
345+
let exitCode = null;
346+
347+
let apiError = new Error('API request failed: 404 - Not Found');
348+
apiError.context = { status: 404 };
349+
350+
let result = await finalizeCommand(
351+
'parallel-123',
352+
{},
353+
{},
354+
{
355+
loadConfig: async () => ({
356+
apiKey: 'test-token',
357+
apiUrl: 'https://api.test',
358+
}),
359+
createApiClient: () => ({ request: async () => ({}) }),
360+
finalizeParallelBuild: async () => {
361+
throw apiError;
362+
},
363+
output,
364+
exit: code => {
365+
exitCode = code;
366+
},
367+
}
368+
);
369+
370+
assert.strictEqual(result.success, true);
371+
assert.strictEqual(result.result.skipped, true);
372+
assert.strictEqual(result.result.reason, 'no-build-found');
373+
assert.strictEqual(exitCode, null);
374+
assert.ok(
375+
output.calls.some(
376+
c => c.method === 'warn' && c.args[0].includes('No build found')
377+
)
378+
);
379+
});
380+
381+
it('fails CI when no build found (404) in strict mode', async () => {
382+
let output = createMockOutput();
383+
let exitCode = null;
384+
385+
let apiError = new Error('API request failed: 404 - Not Found');
386+
apiError.context = { status: 404 };
387+
388+
let result = await finalizeCommand(
389+
'parallel-123',
390+
{},
391+
{ strict: true },
392+
{
393+
loadConfig: async () => ({
394+
apiKey: 'test-token',
395+
apiUrl: 'https://api.test',
396+
}),
397+
createApiClient: () => ({ request: async () => ({}) }),
398+
finalizeParallelBuild: async () => {
399+
throw apiError;
400+
},
401+
output,
402+
exit: code => {
403+
exitCode = code;
404+
},
405+
}
406+
);
407+
408+
assert.strictEqual(result.success, false);
409+
assert.strictEqual(result.reason, 'no-build-found');
410+
assert.strictEqual(exitCode, 1);
411+
assert.ok(
412+
output.calls.some(
413+
c => c.method === 'error' && c.args[0].includes('No build found')
414+
)
415+
);
416+
});
417+
418+
it('shows verbose hints when no build found in non-strict mode', async () => {
419+
let output = createMockOutput();
420+
421+
let apiError = new Error('API request failed: 404 - Not Found');
422+
apiError.context = { status: 404 };
423+
424+
await finalizeCommand(
425+
'parallel-123',
426+
{},
427+
{ verbose: true },
428+
{
429+
loadConfig: async () => ({
430+
apiKey: 'test-token',
431+
apiUrl: 'https://api.test',
432+
}),
433+
createApiClient: () => ({ request: async () => ({}) }),
434+
finalizeParallelBuild: async () => {
435+
throw apiError;
436+
},
437+
output,
438+
exit: () => {},
439+
}
440+
);
441+
442+
// Should show helpful hints in verbose mode
443+
assert.ok(
444+
output.calls.some(
445+
c => c.method === 'info' && c.args[0].includes('Possible reasons')
446+
)
447+
);
448+
assert.ok(
449+
output.calls.some(
450+
c => c.method === 'info' && c.args[0].includes('--strict')
451+
)
452+
);
453+
});
342454
});
343455
});

0 commit comments

Comments
 (0)