@vizzly-testing/cli 0.39.0 → 0.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +50 -35
- package/dist/adapters/cli/setup.js +34 -0
- package/dist/{client → adapters/client}/index.js +6 -7
- package/dist/adapters/http/cloud-proxy.js +29 -0
- package/dist/{server → adapters/http}/http-server.js +12 -13
- package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
- package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
- package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
- package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
- package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
- package/dist/adapters/sdk/index.d.ts +89 -0
- package/dist/{sdk → adapters/sdk}/index.js +22 -19
- package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
- package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
- package/dist/domains/auth/auth-errors.js +50 -0
- package/dist/domains/auth/auth-headers.js +41 -0
- package/dist/domains/auth/auth.normalizers.js +50 -0
- package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
- package/dist/domains/auth/auth.rules.js +19 -0
- package/dist/domains/auth/auth.serializers.js +32 -0
- package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
- package/dist/domains/auth/auth.validations.js +21 -0
- package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
- package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
- package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
- package/dist/domains/auth/token-store.js +81 -0
- package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
- package/dist/domains/baselines/baseline-download.js +192 -0
- package/dist/domains/baselines/baseline-write.js +22 -0
- package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
- package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
- package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
- package/dist/domains/baselines/cloud-baselines.js +9 -0
- package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
- package/dist/domains/builds/build.normalizers.js +35 -0
- package/dist/domains/builds/build.serializers.js +164 -0
- package/dist/domains/builds/build.service.js +119 -0
- package/dist/domains/builds/build.validations.js +39 -0
- package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
- package/dist/domains/builds/cloud-builds.routes.js +31 -0
- package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
- package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
- package/dist/domains/comparisons/cloud-comparisons.js +43 -0
- package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
- package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
- package/dist/domains/config/config-defaults.js +27 -0
- package/dist/{utils → domains/config}/config-discovery.js +1 -9
- package/dist/{utils → domains/config}/config-loader.js +10 -9
- package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
- package/dist/{types → domains/config}/config.d.ts +3 -3
- package/dist/domains/config/config.normalizers.js +126 -0
- package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
- package/dist/domains/config/config.rules.js +47 -0
- package/dist/domains/config/config.serializers.js +202 -0
- package/dist/domains/config/config.service.js +625 -0
- package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
- package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
- package/dist/domains/context/compact-context.validations.js +60 -0
- package/dist/domains/context/context-presentation.js +368 -0
- package/dist/domains/context/context.commands.js +385 -0
- package/dist/domains/context/context.serializers.js +157 -0
- package/dist/domains/context/context.service.js +76 -0
- package/dist/domains/context/context.validations.js +100 -0
- package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
- package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
- package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
- package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
- package/dist/domains/previews/preview.service.js +37 -0
- package/dist/domains/projects/cloud-projects.routes.js +28 -0
- package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
- package/dist/{utils → domains/projects}/project-binding.js +3 -3
- package/dist/domains/projects/project-errors.js +61 -0
- package/dist/{utils → domains/projects}/project-link-store.js +2 -2
- package/dist/domains/projects/project-requests.js +61 -0
- package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
- package/dist/domains/projects/project.normalizers.js +69 -0
- package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
- package/dist/domains/projects/project.validations.js +31 -0
- package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
- package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
- package/dist/domains/reports/dashboard.routes.js +96 -0
- package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
- package/dist/domains/reports/report-assets.routes.js +81 -0
- package/dist/domains/reports/report-state.js +51 -0
- package/dist/domains/reports/report-store.js +85 -0
- package/dist/domains/reports/report.serializers.js +99 -0
- package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
- package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
- package/dist/domains/runs/run-environment.js +69 -0
- package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
- package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
- package/dist/domains/runs/run.normalizers.js +8 -0
- package/dist/domains/runs/run.rules.js +56 -0
- package/dist/domains/runs/run.serializers.js +101 -0
- package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
- package/dist/domains/runs/run.validations.js +17 -0
- package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
- package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
- package/dist/domains/screenshots/cloud-screenshots.js +109 -0
- package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
- package/dist/domains/screenshots/screenshot-hash.js +53 -0
- package/dist/domains/screenshots/screenshot-server-options.js +28 -0
- package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
- package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
- package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
- package/dist/domains/screenshots/screenshot.serializers.js +146 -0
- package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
- package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
- package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
- package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
- package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
- package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
- package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
- package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
- package/dist/domains/updates/cli-update.js +103 -0
- package/dist/domains/updates/update-check.js +2 -0
- package/dist/domains/updates/update-notifications.js +175 -0
- package/dist/domains/uploads/upload-defaults.js +4 -0
- package/dist/domains/uploads/upload-files.js +32 -0
- package/dist/domains/uploads/upload-settings.js +21 -0
- package/dist/domains/uploads/upload-timing.js +18 -0
- package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
- package/dist/domains/uploads/upload.normalizers.js +60 -0
- package/dist/domains/uploads/upload.serializers.js +179 -0
- package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
- package/dist/domains/uploads/upload.validations.js +77 -0
- package/dist/index.js +12 -12
- package/dist/reporter/reporter-bundle.iife.js +86 -86
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/shared/cloud-protocol.js +171 -0
- package/dist/{utils → shared}/file-changes.js +1 -1
- package/dist/shared/files.js +9 -0
- package/dist/{utils → shared}/global-config.js +7 -78
- package/dist/shared/log-level.js +7 -0
- package/dist/{utils → shared}/output.js +1 -1
- package/dist/{utils → shared}/package-info.js +5 -4
- package/dist/shared/paths.js +57 -0
- package/package.json +37 -36
- package/dist/api/core.js +0 -382
- package/dist/api/endpoints.js +0 -421
- package/dist/api/index.js +0 -19
- package/dist/auth/core.js +0 -237
- package/dist/auth/index.js +0 -31
- package/dist/commands/context.js +0 -1066
- package/dist/config/core.js +0 -439
- package/dist/config/operations.js +0 -327
- package/dist/project/core.js +0 -211
- package/dist/screenshot-server/core.js +0 -161
- package/dist/screenshot-server/index.js +0 -11
- package/dist/server/routers/assets.js +0 -95
- package/dist/server/routers/cloud-proxy.js +0 -146
- package/dist/server/routers/dashboard.js +0 -172
- package/dist/services/auth-service.js +0 -117
- package/dist/services/config-service.js +0 -313
- package/dist/services/project-service.js +0 -88
- package/dist/test-runner/core.js +0 -293
- package/dist/test-runner/index.js +0 -13
- package/dist/types/sdk.d.ts +0 -186
- package/dist/uploader/core.js +0 -432
- package/dist/uploader/index.js +0 -88
- /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
- /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
- /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
- /package/dist/{server → adapters/http}/middleware/response.js +0 -0
- /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
- /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
- /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
- /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
- /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
- /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
- /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
- /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
- /package/dist/{context → domains/context}/provider-resolver.js +0 -0
- /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
- /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
- /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
- /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
- /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
- /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
- /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
- /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
- /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{utils → shared}/api-url.js +0 -0
- /package/dist/{utils → shared}/async-utils.js +0 -0
- /package/dist/{utils → shared}/browser.js +0 -0
- /package/dist/{utils → shared}/ci-env.js +0 -0
- /package/dist/{utils → shared}/colors.js +0 -0
- /package/dist/{utils → shared}/fetch-utils.js +0 -0
- /package/dist/{utils → shared}/git.js +0 -0
- /package/dist/{utils → shared}/patterns.js +0 -0
- /package/dist/{utils → shared}/secret-store.js +0 -0
- /package/dist/{errors → shared}/vizzly-error.js +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync as defaultExistsSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
2
|
+
import { fetchWithTimeout as defaultFetchWithTimeout } from '../../shared/fetch-utils.js';
|
|
3
|
+
import { safePath as defaultSafePath } from '../../shared/paths.js';
|
|
4
|
+
import { NetworkError } from '../../shared/vizzly-error.js';
|
|
5
|
+
import { sanitizeScreenshotName as defaultSanitizeScreenshotName } from '../screenshots/screenshot.validations.js';
|
|
6
|
+
import { saveBaselineMetadata as defaultSaveBaselineMetadata } from './baseline-metadata.js';
|
|
7
|
+
|
|
8
|
+
/** Install one selected cloud build without changing how it was selected. */
|
|
9
|
+
export async function installBaselineBuild({
|
|
10
|
+
baselineBuild,
|
|
11
|
+
baselinePath,
|
|
12
|
+
workingDir,
|
|
13
|
+
threshold,
|
|
14
|
+
signatureProperties,
|
|
15
|
+
existingBaseline,
|
|
16
|
+
environment,
|
|
17
|
+
branch,
|
|
18
|
+
output,
|
|
19
|
+
fetchWithTimeout = defaultFetchWithTimeout,
|
|
20
|
+
operations = {}
|
|
21
|
+
}) {
|
|
22
|
+
let {
|
|
23
|
+
existsSync = defaultExistsSync,
|
|
24
|
+
writeFileSync = defaultWriteFileSync,
|
|
25
|
+
safePath = defaultSafePath,
|
|
26
|
+
sanitizeScreenshotName = defaultSanitizeScreenshotName,
|
|
27
|
+
saveBaselineMetadata = defaultSaveBaselineMetadata
|
|
28
|
+
} = operations;
|
|
29
|
+
let existingShaMap = new Map();
|
|
30
|
+
if (existingBaseline) {
|
|
31
|
+
existingBaseline.screenshots.forEach(screenshot => {
|
|
32
|
+
if (screenshot.sha256 && screenshot.filename) {
|
|
33
|
+
existingShaMap.set(screenshot.filename, screenshot.sha256);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Download screenshots
|
|
39
|
+
let downloadedCount = 0;
|
|
40
|
+
let skippedCount = 0;
|
|
41
|
+
let errorCount = 0;
|
|
42
|
+
let batchSize = 5;
|
|
43
|
+
let screenshotsToProcess = [];
|
|
44
|
+
for (let screenshot of baselineBuild.screenshots) {
|
|
45
|
+
let sanitizedName;
|
|
46
|
+
try {
|
|
47
|
+
sanitizedName = sanitizeScreenshotName(screenshot.name);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
output.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
|
|
50
|
+
errorCount++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let filename = screenshot.filename;
|
|
54
|
+
if (!filename) {
|
|
55
|
+
output.warn(`Screenshot ${sanitizedName} has no filename from API - skipping`);
|
|
56
|
+
errorCount++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let imagePath = safePath(baselinePath, filename);
|
|
60
|
+
|
|
61
|
+
// Check SHA
|
|
62
|
+
if (existsSync(imagePath) && screenshot.sha256) {
|
|
63
|
+
let storedSha = existingShaMap.get(filename);
|
|
64
|
+
if (storedSha === screenshot.sha256) {
|
|
65
|
+
downloadedCount++;
|
|
66
|
+
skippedCount++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let downloadUrl = screenshot.original_url || screenshot.url;
|
|
71
|
+
if (!downloadUrl) {
|
|
72
|
+
output.warn(`Screenshot ${sanitizedName} has no download URL - skipping`);
|
|
73
|
+
errorCount++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
screenshotsToProcess.push({
|
|
77
|
+
sanitizedName,
|
|
78
|
+
imagePath,
|
|
79
|
+
downloadUrl
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Process downloads in batches
|
|
84
|
+
if (screenshotsToProcess.length > 0) {
|
|
85
|
+
output.info(`📥 Downloading ${screenshotsToProcess.length} new/updated screenshots...`);
|
|
86
|
+
for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
|
|
87
|
+
let batch = screenshotsToProcess.slice(i, i + batchSize);
|
|
88
|
+
let batchNum = Math.floor(i / batchSize) + 1;
|
|
89
|
+
let totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
|
|
90
|
+
output.info(`📦 Processing batch ${batchNum}/${totalBatches}`);
|
|
91
|
+
let downloadPromises = batch.map(async ({
|
|
92
|
+
sanitizedName,
|
|
93
|
+
imagePath,
|
|
94
|
+
downloadUrl
|
|
95
|
+
}) => {
|
|
96
|
+
try {
|
|
97
|
+
let response = await fetchWithTimeout(downloadUrl);
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new NetworkError(`Failed to download ${sanitizedName}: ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
let arrayBuffer = await response.arrayBuffer();
|
|
102
|
+
let imageBuffer = Buffer.from(arrayBuffer);
|
|
103
|
+
writeFileSync(imagePath, imageBuffer);
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
name: sanitizedName
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
output.warn(`Failed to download ${sanitizedName}: ${error.message}`);
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
name: sanitizedName,
|
|
113
|
+
error: error.message
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
let batchResults = await Promise.all(downloadPromises);
|
|
118
|
+
let batchSuccesses = batchResults.filter(r => r.success).length;
|
|
119
|
+
let batchFailures = batchResults.filter(r => !r.success).length;
|
|
120
|
+
downloadedCount += batchSuccesses;
|
|
121
|
+
errorCount += batchFailures;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (downloadedCount === 0 && skippedCount === 0) {
|
|
125
|
+
output.error('No screenshots were successfully downloaded');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Store baseline metadata
|
|
130
|
+
let baselineData = {
|
|
131
|
+
buildId: baselineBuild.id,
|
|
132
|
+
buildName: baselineBuild.name,
|
|
133
|
+
environment,
|
|
134
|
+
branch,
|
|
135
|
+
threshold,
|
|
136
|
+
signatureProperties,
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
buildInfo: {
|
|
139
|
+
commitSha: baselineBuild.commit_sha,
|
|
140
|
+
commitMessage: baselineBuild.commit_message,
|
|
141
|
+
visual_review: baselineBuild.visual_review || null,
|
|
142
|
+
completedAt: baselineBuild.completed_at
|
|
143
|
+
},
|
|
144
|
+
screenshots: baselineBuild.screenshots.filter(s => s.filename).map(s => ({
|
|
145
|
+
name: sanitizeScreenshotName(s.name),
|
|
146
|
+
originalName: s.name,
|
|
147
|
+
sha256: s.sha256,
|
|
148
|
+
id: s.id,
|
|
149
|
+
filename: s.filename,
|
|
150
|
+
path: safePath(baselinePath, s.filename),
|
|
151
|
+
browser: s.browser,
|
|
152
|
+
viewport_width: s.viewport_width,
|
|
153
|
+
originalUrl: s.original_url,
|
|
154
|
+
fileSize: s.file_size_bytes,
|
|
155
|
+
dimensions: {
|
|
156
|
+
width: s.width,
|
|
157
|
+
height: s.height
|
|
158
|
+
}
|
|
159
|
+
}))
|
|
160
|
+
};
|
|
161
|
+
saveBaselineMetadata(baselinePath, baselineData);
|
|
162
|
+
|
|
163
|
+
// Save baseline build metadata for MCP plugin
|
|
164
|
+
let baselineMetadataPath = safePath(workingDir, '.vizzly', 'baseline-metadata.json');
|
|
165
|
+
writeFileSync(baselineMetadataPath, JSON.stringify({
|
|
166
|
+
buildId: baselineBuild.id,
|
|
167
|
+
buildName: baselineBuild.name,
|
|
168
|
+
branch,
|
|
169
|
+
environment,
|
|
170
|
+
commitSha: baselineBuild.commit_sha,
|
|
171
|
+
commitMessage: baselineBuild.commit_message,
|
|
172
|
+
visual_review: baselineBuild.visual_review || null,
|
|
173
|
+
completedAt: baselineBuild.completed_at,
|
|
174
|
+
downloadedAt: new Date().toISOString()
|
|
175
|
+
}, null, 2));
|
|
176
|
+
|
|
177
|
+
// Summary
|
|
178
|
+
let actualDownloads = downloadedCount - skippedCount;
|
|
179
|
+
if (skippedCount > 0) {
|
|
180
|
+
if (actualDownloads === 0) {
|
|
181
|
+
output.info(`All ${skippedCount} baselines up-to-date`);
|
|
182
|
+
} else {
|
|
183
|
+
output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
output.info(`Downloaded ${downloadedCount}/${baselineBuild.screenshots.length} screenshots successfully`);
|
|
187
|
+
}
|
|
188
|
+
if (errorCount > 0) {
|
|
189
|
+
output.warn(`${errorCount} screenshots failed to download`);
|
|
190
|
+
}
|
|
191
|
+
return baselineData;
|
|
192
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
2
|
+
import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from './baseline-metadata.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write one image and update its baseline record. The caller commits metadata
|
|
6
|
+
* after a single approval or after its full bulk update has finished.
|
|
7
|
+
*/
|
|
8
|
+
export function writeBaselineScreenshot({
|
|
9
|
+
imageBuffer,
|
|
10
|
+
screenshot,
|
|
11
|
+
metadata,
|
|
12
|
+
baselineOptions
|
|
13
|
+
}, {
|
|
14
|
+
writeFileSync = defaultWriteFileSync,
|
|
15
|
+
createEmptyBaselineMetadata = defaultCreateEmptyBaselineMetadata,
|
|
16
|
+
upsertScreenshotInMetadata = defaultUpsertScreenshotInMetadata
|
|
17
|
+
} = {}) {
|
|
18
|
+
writeFileSync(screenshot.path, imageBuffer);
|
|
19
|
+
let baselineData = metadata || createEmptyBaselineMetadata(baselineOptions);
|
|
20
|
+
upsertScreenshotInMetadata(baselineData, screenshot, screenshot.signature);
|
|
21
|
+
return baselineData;
|
|
22
|
+
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Handles baseline management (accept, accept-all, reset)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
6
|
+
import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
|
|
7
|
+
import { sendError, sendServiceUnavailable, sendSuccess } from '../../adapters/http/middleware/response.js';
|
|
8
|
+
import * as output from '../../shared/output.js';
|
|
9
9
|
function sendBaselineUnavailable(res) {
|
|
10
10
|
sendError(res, 400, 'Baseline management not available');
|
|
11
11
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* of baseline storage without any network operations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -148,42 +148,4 @@ export function getCurrentPath(currentPath, filename) {
|
|
|
148
148
|
*/
|
|
149
149
|
export function getDiffPath(diffPath, filename) {
|
|
150
150
|
return join(diffPath, filename);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Promote current screenshot to baseline (accept as new baseline)
|
|
155
|
-
*
|
|
156
|
-
* @param {string} currentPath - Path to current screenshots directory
|
|
157
|
-
* @param {string} baselinePath - Path to baselines directory
|
|
158
|
-
* @param {string} filename - Filename
|
|
159
|
-
*/
|
|
160
|
-
export function promoteCurrentToBaseline(currentPath, baselinePath, filename) {
|
|
161
|
-
let currentFile = join(currentPath, filename);
|
|
162
|
-
let baselineFile = join(baselinePath, filename);
|
|
163
|
-
if (!existsSync(currentFile)) {
|
|
164
|
-
throw new Error(`Current screenshot not found: ${currentFile}`);
|
|
165
|
-
}
|
|
166
|
-
copyFileSync(currentFile, baselineFile);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Read baseline image
|
|
171
|
-
*
|
|
172
|
-
* @param {string} baselinePath - Path to baselines directory
|
|
173
|
-
* @param {string} filename - Filename
|
|
174
|
-
* @returns {Buffer}
|
|
175
|
-
*/
|
|
176
|
-
export function readBaseline(baselinePath, filename) {
|
|
177
|
-
return readFileSync(join(baselinePath, filename));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Read current screenshot
|
|
182
|
-
*
|
|
183
|
-
* @param {string} currentPath - Path to current screenshots directory
|
|
184
|
-
* @param {string} filename - Filename
|
|
185
|
-
* @returns {Buffer}
|
|
186
|
-
*/
|
|
187
|
-
export function readCurrent(currentPath, filename) {
|
|
188
|
-
return readFileSync(join(currentPath, filename));
|
|
189
151
|
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync as defaultExistsSync, readdirSync as defaultReaddirSync, statSync as defaultStatSync } from 'node:fs';
|
|
6
6
|
import { basename, join, resolve } from 'node:path';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
7
|
+
import * as defaultOutput from '../../shared/output.js';
|
|
8
|
+
import { createWildcardMatcher } from '../../shared/patterns.js';
|
|
9
|
+
import { loadBaselineMetadata as defaultLoadBaselineMetadata } from './baseline-metadata.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Extract filename from a path
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get TDD baselines for a build
|
|
3
|
+
* @param {Object} client - API client
|
|
4
|
+
* @param {string} buildId - Build ID
|
|
5
|
+
* @returns {Promise<Object>} { build, screenshots, signatureProperties }
|
|
6
|
+
*/
|
|
7
|
+
export async function getTddBaselines(client, buildId) {
|
|
8
|
+
return client.request(`/api/sdk/builds/${buildId}/tdd-baselines`);
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
|
|
2
|
+
import { sendError, sendSuccess } from '../../adapters/http/middleware/response.js';
|
|
3
|
+
import * as output from '../../shared/output.js';
|
|
4
|
+
export function createCloudBaselinesRouter({
|
|
5
|
+
authService
|
|
6
|
+
}) {
|
|
7
|
+
return async function handleCloudBaselinesRoute(req, res, pathname) {
|
|
8
|
+
if (req.method !== 'POST' || pathname !== '/api/cloud/baselines/download') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
let body = await parseJsonBody(req);
|
|
13
|
+
let {
|
|
14
|
+
buildId,
|
|
15
|
+
screenshotNames
|
|
16
|
+
} = body;
|
|
17
|
+
if (!buildId) {
|
|
18
|
+
sendError(res, 400, 'buildId is required');
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
let response = await authService.authenticatedRequest('/api/cli/baselines/download', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json'
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
buildId,
|
|
28
|
+
screenshotNames
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
sendSuccess(res, {
|
|
32
|
+
success: true,
|
|
33
|
+
message: `Baselines downloaded from build ${buildId}`,
|
|
34
|
+
...response
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
output.error('Error downloading baselines from cloud:', error);
|
|
38
|
+
sendError(res, 500, error.message);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getVisualReviewState } from '../context/context.normalizers.js';
|
|
2
|
+
export function normalizeBuildStatus(buildStatus) {
|
|
3
|
+
return buildStatus.build;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read the processing counts from the status response.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} status - Status response.
|
|
10
|
+
* @returns {Object} API-provided processing facts.
|
|
11
|
+
*/
|
|
12
|
+
export function getProcessingStatus(status = {}) {
|
|
13
|
+
return status.processing;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read comparison totals without deriving them from screenshot counts.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} status - Status response.
|
|
20
|
+
* @returns {Object} API-provided comparison facts.
|
|
21
|
+
*/
|
|
22
|
+
export function getComparisonStatus(status = {}) {
|
|
23
|
+
return status.comparisons;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read the build review state without mixing it with processing status.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} status - Status response.
|
|
30
|
+
* @returns {string|null} API-provided review state when available.
|
|
31
|
+
*/
|
|
32
|
+
export function getBuildReviewState(status = {}) {
|
|
33
|
+
let build = normalizeBuildStatus(status);
|
|
34
|
+
return getVisualReviewState(build);
|
|
35
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getComparisonStatus, getProcessingStatus, normalizeBuildStatus } from './build.normalizers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build payload for build creation
|
|
5
|
+
* @param {Object} options - Build options
|
|
6
|
+
* @returns {Object} Build creation payload
|
|
7
|
+
*/
|
|
8
|
+
export function buildBuildPayload(options) {
|
|
9
|
+
let payload = {
|
|
10
|
+
name: options.name || options.buildName,
|
|
11
|
+
branch: options.branch,
|
|
12
|
+
environment: options.environment
|
|
13
|
+
};
|
|
14
|
+
if (options.commit || options.commit_sha) {
|
|
15
|
+
payload.commit_sha = options.commit || options.commit_sha;
|
|
16
|
+
}
|
|
17
|
+
if (options.message || options.commit_message) {
|
|
18
|
+
payload.commit_message = options.message || options.commit_message;
|
|
19
|
+
}
|
|
20
|
+
if (options.commit_author_name) {
|
|
21
|
+
payload.commit_author_name = options.commit_author_name;
|
|
22
|
+
}
|
|
23
|
+
if (options.commit_author_email) {
|
|
24
|
+
payload.commit_author_email = options.commit_author_email;
|
|
25
|
+
}
|
|
26
|
+
if (options.pullRequestNumber || options.github_pull_request_number) {
|
|
27
|
+
payload.github_pull_request_number = options.pullRequestNumber || options.github_pull_request_number;
|
|
28
|
+
}
|
|
29
|
+
if (options.parallelId || options.parallel_id) {
|
|
30
|
+
payload.parallel_id = options.parallelId || options.parallel_id;
|
|
31
|
+
}
|
|
32
|
+
if (options.threshold != null) {
|
|
33
|
+
payload.threshold = options.threshold;
|
|
34
|
+
}
|
|
35
|
+
if (options.metadata) {
|
|
36
|
+
payload.metadata = options.metadata;
|
|
37
|
+
}
|
|
38
|
+
return payload;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format a build for JSON output
|
|
43
|
+
*/
|
|
44
|
+
export function formatBuildForJson(build, includeComparisons = false) {
|
|
45
|
+
let result = {
|
|
46
|
+
id: build.id,
|
|
47
|
+
name: build.name,
|
|
48
|
+
status: build.status,
|
|
49
|
+
branch: build.branch,
|
|
50
|
+
commit: build.commit_sha,
|
|
51
|
+
commitMessage: build.commit_message,
|
|
52
|
+
environment: build.environment,
|
|
53
|
+
screenshotCount: build.screenshot_count || 0,
|
|
54
|
+
comparisons: {
|
|
55
|
+
total: build.total_comparisons || 0,
|
|
56
|
+
new: build.new_comparisons || 0,
|
|
57
|
+
changed: build.changed_comparisons || 0,
|
|
58
|
+
identical: build.identical_comparisons || 0
|
|
59
|
+
},
|
|
60
|
+
visual_review: build.visual_review || null,
|
|
61
|
+
createdAt: build.created_at,
|
|
62
|
+
completedAt: build.completed_at
|
|
63
|
+
};
|
|
64
|
+
if (includeComparisons && build.comparisons) {
|
|
65
|
+
result.comparisonDetails = build.comparisons.map(c => {
|
|
66
|
+
let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null;
|
|
67
|
+
let diffImage = c.diff_image || {};
|
|
68
|
+
let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null;
|
|
69
|
+
let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null;
|
|
70
|
+
let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null;
|
|
71
|
+
let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null;
|
|
72
|
+
let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
|
|
73
|
+
return {
|
|
74
|
+
id: c.id,
|
|
75
|
+
name: c.name || c.current_name,
|
|
76
|
+
status: c.status,
|
|
77
|
+
diffPercentage: c.diff_percentage,
|
|
78
|
+
visual_review: c.visual_review || null,
|
|
79
|
+
urls: {
|
|
80
|
+
baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
|
|
81
|
+
current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
|
|
82
|
+
diff: diffUrl
|
|
83
|
+
},
|
|
84
|
+
honeydiff: hasHoneydiff ? {
|
|
85
|
+
ssimScore,
|
|
86
|
+
gmsdScore,
|
|
87
|
+
clusterClassification: clusterMetadata?.classification || null,
|
|
88
|
+
clusterMetadata,
|
|
89
|
+
fingerprintHash,
|
|
90
|
+
diffRegions: c.diff_regions ?? diffImage.diff_regions ?? null
|
|
91
|
+
} : null
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Give agents concrete paths from lifecycle status into visual evidence.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} build - Build record from the status response.
|
|
102
|
+
* @returns {{label: string, command: string}[]} Executable CLI commands.
|
|
103
|
+
*/
|
|
104
|
+
export function createStatusSuggestedCommands(build = {}) {
|
|
105
|
+
if (!build.id) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
return [{
|
|
109
|
+
label: 'Discover build context API',
|
|
110
|
+
command: 'vizzly api schema sdk.getBuildContext --json'
|
|
111
|
+
}, {
|
|
112
|
+
label: 'List comparisons',
|
|
113
|
+
command: `vizzly --json comparisons --build ${build.id}`
|
|
114
|
+
}];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every lifecycle, processing, comparison, and review value comes directly
|
|
119
|
+
* from the API.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} status - Status response.
|
|
122
|
+
* @param {Object|null} previewInfo - Optional preview response.
|
|
123
|
+
* @returns {Object} Machine-readable status payload.
|
|
124
|
+
*/
|
|
125
|
+
export function createStatusData(status, previewInfo = null) {
|
|
126
|
+
let build = normalizeBuildStatus(status);
|
|
127
|
+
let processing = getProcessingStatus(status);
|
|
128
|
+
let comparisons = getComparisonStatus(status);
|
|
129
|
+
return {
|
|
130
|
+
resource: status.resource,
|
|
131
|
+
schemaVersion: status.schema_version,
|
|
132
|
+
buildId: build.id,
|
|
133
|
+
status: build.status,
|
|
134
|
+
conclusion: status.conclusion,
|
|
135
|
+
name: build.name,
|
|
136
|
+
createdAt: build.created_at,
|
|
137
|
+
updatedAt: build.updated_at,
|
|
138
|
+
completedAt: build.completed_at,
|
|
139
|
+
environment: build.environment,
|
|
140
|
+
branch: build.branch,
|
|
141
|
+
commit: build.commit_sha,
|
|
142
|
+
commitMessage: build.commit_message,
|
|
143
|
+
screenshotsTotal: processing.total,
|
|
144
|
+
processing,
|
|
145
|
+
comparisonsTotal: comparisons.total,
|
|
146
|
+
comparisons,
|
|
147
|
+
newComparisons: comparisons.new,
|
|
148
|
+
changedComparisons: comparisons.changed,
|
|
149
|
+
identicalComparisons: comparisons.identical,
|
|
150
|
+
visual_review: build.visual_review || null,
|
|
151
|
+
executionTime: build.execution_time_ms,
|
|
152
|
+
isBaseline: build.is_baseline,
|
|
153
|
+
userAgent: build.user_agent,
|
|
154
|
+
scope: status.scope,
|
|
155
|
+
links: status.links,
|
|
156
|
+
suggestedCommands: createStatusSuggestedCommands(build),
|
|
157
|
+
preview: previewInfo ? {
|
|
158
|
+
url: previewInfo.preview_url,
|
|
159
|
+
status: previewInfo.status,
|
|
160
|
+
fileCount: previewInfo.file_count,
|
|
161
|
+
expiresAt: previewInfo.expires_at
|
|
162
|
+
} : null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { buildEndpointWithParams, buildQueryParams } from '../../shared/cloud-protocol.js';
|
|
2
|
+
import { buildBuildPayload } from './build.serializers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get build information
|
|
6
|
+
* @param {Object} client - API client
|
|
7
|
+
* @param {string} buildId - Build ID
|
|
8
|
+
* @param {string|null} include - Optional include parameter (e.g., 'screenshots')
|
|
9
|
+
* @returns {Promise<Object>} Build data
|
|
10
|
+
*/
|
|
11
|
+
export async function getBuild(client, buildId, include = null) {
|
|
12
|
+
let endpoint = `/api/sdk/builds/${buildId}`;
|
|
13
|
+
if (include) {
|
|
14
|
+
endpoint = buildEndpointWithParams(endpoint, {
|
|
15
|
+
include
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return client.request(endpoint);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the server-owned build processing and review status.
|
|
23
|
+
*
|
|
24
|
+
* This endpoint keeps processing, comparison, and review counts separate so
|
|
25
|
+
* callers do not have to reconstruct lifecycle state from legacy build fields.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} client - API client
|
|
28
|
+
* @param {string} buildId - Build ID
|
|
29
|
+
* @returns {Promise<Object>} Build status response
|
|
30
|
+
*/
|
|
31
|
+
export async function getBuildStatus(client, buildId) {
|
|
32
|
+
return client.request(`/api/sdk/builds/${buildId}/status`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get builds for a project
|
|
37
|
+
* @param {Object} client - API client
|
|
38
|
+
* @param {Object} filters - Filter options
|
|
39
|
+
* @returns {Promise<Array>} List of builds
|
|
40
|
+
*/
|
|
41
|
+
export async function getBuilds(client, filters = {}) {
|
|
42
|
+
let query = buildQueryParams(filters);
|
|
43
|
+
let endpoint = `/api/sdk/builds${query ? `?${query}` : ''}`;
|
|
44
|
+
return client.request(endpoint);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new build
|
|
49
|
+
* @param {Object} client - API client
|
|
50
|
+
* @param {Object} metadata - Build metadata
|
|
51
|
+
* @returns {Promise<Object>} Created build data
|
|
52
|
+
*/
|
|
53
|
+
export async function createBuild(client, metadata) {
|
|
54
|
+
let payload = buildBuildPayload(metadata);
|
|
55
|
+
return client.request('/api/sdk/builds', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json'
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
build: payload
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update build status
|
|
68
|
+
* @param {Object} client - API client
|
|
69
|
+
* @param {string} buildId - Build ID
|
|
70
|
+
* @param {string} status - Build status (pending|running|completed|failed)
|
|
71
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
72
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
73
|
+
* @returns {Promise<Object>} Updated build data
|
|
74
|
+
*/
|
|
75
|
+
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null, failureReason) {
|
|
76
|
+
let body = {
|
|
77
|
+
status
|
|
78
|
+
};
|
|
79
|
+
if (status === 'failed' && failureReason) body.failureReason = failureReason;
|
|
80
|
+
if (executionTimeMs != null) {
|
|
81
|
+
body.executionTimeMs = executionTimeMs;
|
|
82
|
+
}
|
|
83
|
+
return client.request(`/api/sdk/builds/${buildId}/status`, {
|
|
84
|
+
method: 'PUT',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(body)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mark a build completed or failed, with an optional failure explanation.
|
|
94
|
+
* @param {Object} client - API client
|
|
95
|
+
* @param {string} buildId - Build ID
|
|
96
|
+
* @param {boolean} [success=true] - Whether tests and uploads succeeded
|
|
97
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
98
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
99
|
+
* @returns {Promise<Object>} Finalized build data
|
|
100
|
+
*/
|
|
101
|
+
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null, failureReason) {
|
|
102
|
+
let status = success ? 'completed' : 'failed';
|
|
103
|
+
return updateBuildStatus(client, buildId, status, executionTimeMs, failureReason);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Finalize a parallel build
|
|
108
|
+
* @param {Object} client - API client
|
|
109
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
110
|
+
* @returns {Promise<Object>} Finalization result
|
|
111
|
+
*/
|
|
112
|
+
export async function finalizeParallelBuild(client, parallelId) {
|
|
113
|
+
return client.request(`/api/sdk/parallel/${parallelId}/finalize`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json'
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|