-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfinalize.js
More file actions
195 lines (173 loc) · 5.46 KB
/
finalize.js
File metadata and controls
195 lines (173 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* Finalize command implementation
* Uses functional API operations directly
*/
import {
createApiClient as defaultCreateApiClient,
finalizeParallelBuild as defaultFinalizeParallelBuild,
} from '../api/index.js';
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import * as defaultOutput from '../utils/output.js';
import {
resolveProjectTarget as defaultResolveProjectTarget,
validateTargetOptions,
} from '../utils/project-target.js';
import { writeSession as defaultWriteSession } from '../utils/session.js';
let MISSING_BUILD_HINTS = [
' • No screenshots were uploaded with this parallel-id',
' • Tests were skipped or failed before capturing screenshots',
' • The parallel-id does not match what was used during test runs',
];
/**
* Finalize command implementation
* @param {string} parallelId - Parallel ID to finalize
* @param {Object} options - Command options
* @param {Object} globalOptions - Global CLI options
* @param {Object} deps - Dependencies for testing
*/
export async function finalizeCommand(
parallelId,
options = {},
globalOptions = {},
deps = {}
) {
let {
loadConfig = defaultLoadConfig,
createApiClient = defaultCreateApiClient,
finalizeParallelBuild = defaultFinalizeParallelBuild,
resolveProjectTarget = defaultResolveProjectTarget,
output = defaultOutput,
writeSession = defaultWriteSession,
exit = code => process.exit(code),
} = deps;
output.configure({
json: globalOptions.json,
verbose: globalOptions.verbose,
color: !globalOptions.noColor,
});
try {
// Load configuration with CLI overrides
let allOptions = { ...globalOptions, ...options };
let config = await loadConfig(globalOptions.config, allOptions);
// Validate API token
if (!config.apiKey) {
output.error(
'API token required. Use --token or set VIZZLY_TOKEN environment variable'
);
exit(1);
return { success: false, reason: 'no-api-key' };
}
let resolvedTarget = await resolveProjectTarget({
command: 'finalize',
options,
config,
requireTarget: true,
});
if (resolvedTarget.target) {
config.target = resolvedTarget.target;
}
output.debug('target', {
command: 'finalize',
source: resolvedTarget.source,
target: resolvedTarget.target,
});
if (globalOptions.verbose) {
output.info('Configuration loaded');
output.debug('Config details', {
parallelId,
apiUrl: config.apiUrl,
});
}
// Call finalize endpoint via functional API
output.startSpinner('Finalizing parallel build...');
let client = createApiClient({
baseUrl: config.apiUrl,
token: config.apiKey,
command: 'finalize',
});
let result = await finalizeParallelBuild(client, parallelId, {
target: config.target,
});
output.stopSpinner();
// Write session for subsequent commands (like preview)
if (result.build?.id) {
writeSession({
buildId: result.build.id,
parallelId,
});
}
if (globalOptions.json) {
output.data(result);
} else {
output.header('finalize');
output.complete(`Parallel build finalized`);
output.blank();
output.keyValue({
Build: result.build.id,
Status: result.build.status,
'Parallel ID': result.build.parallel_id,
});
}
return { success: true, result };
} catch (error) {
output.stopSpinner();
let status = error.context?.status;
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
// Note: --strict does NOT affect 5xx handling - infrastructure issues are out of user's control
if (status >= 500) {
output.warn('Vizzly API unavailable - finalize skipped.');
return {
success: true,
result: { skipped: true, reason: 'api-unavailable' },
};
}
// Handle missing builds gracefully (404 errors)
// This happens when: no screenshots were uploaded, tests were skipped, or parallel-id doesn't exist
if (status === 404) {
let isStrict = globalOptions.strict;
if (isStrict) {
output.error(`No build found for parallel ID: ${parallelId}`);
output.blank();
output.info('This can happen when:');
for (let hint of MISSING_BUILD_HINTS) {
output.info(hint);
}
exit(1);
return { success: false, reason: 'no-build-found', error };
}
// Non-strict mode: warn but don't fail CI
output.warn(
`No build found for parallel ID: ${parallelId} - finalize skipped.`
);
if (globalOptions.verbose) {
output.info('Possible reasons:');
for (let hint of MISSING_BUILD_HINTS) {
output.info(hint);
}
output.info('Use --strict flag to fail CI when no build is found.');
}
return {
success: true,
result: { skipped: true, reason: 'no-build-found' },
};
}
output.error('Failed to finalize parallel build', error);
exit(1);
return { success: false, error };
} finally {
output.cleanup();
}
}
/**
* Validate finalize options
* @param {string} parallelId - Parallel ID to finalize
* @param {Object} options - Command options
*/
export function validateFinalizeOptions(parallelId, _options) {
let errors = [];
if (!parallelId || parallelId.trim() === '') {
errors.push('Parallel ID is required');
}
errors.push(...validateTargetOptions(_options));
return errors;
}