@scrymore/scry-deployer 0.1.0 → 0.1.1
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/bin/cli.js +51 -80
- package/lib/apiClient.js +52 -2
- package/lib/coverage.js +32 -9
- package/package.json +2 -2
package/bin/cli.js
CHANGED
|
@@ -85,90 +85,46 @@ async function runDeployment(argv) {
|
|
|
85
85
|
logger.debug(`Received arguments: ${JSON.stringify(argv)}`);
|
|
86
86
|
|
|
87
87
|
const outPath = path.join(os.tmpdir(), `storybook-deployment-${Date.now()}.zip`);
|
|
88
|
+
let metadataZipPath = null;
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
|
-
const
|
|
91
|
+
const coverage = await resolveCoverage(argv, logger);
|
|
92
|
+
const coverageReport = coverage.coverageReport;
|
|
93
|
+
const coverageSummary = coverage.coverageSummary;
|
|
94
|
+
metadataZipPath = coverage.metadataZipPath;
|
|
91
95
|
|
|
92
96
|
if (argv.withAnalysis) {
|
|
93
|
-
// Full deployment with analysis
|
|
94
97
|
logger.info('Running deployment with analysis...');
|
|
98
|
+
}
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
logger.info('1/5: Skipping screenshot capture (no Storybook URL provided)');
|
|
103
|
-
}
|
|
100
|
+
// 1. Archive only the static Storybook files.
|
|
101
|
+
logger.info(`1/3: Zipping directory '${argv.dir}'...`);
|
|
102
|
+
await zipDirectory(argv.dir, outPath);
|
|
103
|
+
logger.success(`✅ Archive created: ${outPath}`);
|
|
104
|
+
logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// 2. Upload Storybook ZIP + coverage + metadata ZIP (if present).
|
|
107
|
+
logger.info('2/3: Uploading to deployment service...');
|
|
108
|
+
const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
|
|
109
|
+
const uploadResult = await uploadBuild(
|
|
110
|
+
apiClient,
|
|
111
|
+
{
|
|
110
112
|
project: argv.project,
|
|
111
|
-
version: argv.version
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
metadata: analysisResults
|
|
122
|
-
});
|
|
123
|
-
logger.success(`✅ Master archive created: ${outPath}`);
|
|
124
|
-
logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
|
|
125
|
-
|
|
126
|
-
// 4. Upload archive (+ optional coverage)
|
|
127
|
-
logger.info('4/5: Uploading to deployment service...');
|
|
128
|
-
const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
|
|
129
|
-
const uploadResult = await uploadBuild(
|
|
130
|
-
apiClient,
|
|
131
|
-
{
|
|
132
|
-
project: argv.project,
|
|
133
|
-
version: argv.version,
|
|
134
|
-
},
|
|
135
|
-
{ zipPath: outPath, coverageReport }
|
|
136
|
-
);
|
|
137
|
-
logger.success('✅ Archive uploaded.');
|
|
138
|
-
logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
|
|
139
|
-
|
|
140
|
-
await postPRComment(buildDeployResult(argv, coverageSummary, uploadResult), coverageSummary);
|
|
141
|
-
|
|
142
|
-
logger.success('\n🎉 Deployment with analysis successful! 🎉');
|
|
143
|
-
logUploadLinks(argv, coverageSummary, uploadResult, logger);
|
|
113
|
+
version: argv.version,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
zipPath: outPath,
|
|
117
|
+
coverageReport,
|
|
118
|
+
metadataZipPath,
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
logger.success('✅ Archive uploaded.');
|
|
122
|
+
logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
|
|
144
123
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await zipDirectory(argv.dir, outPath);
|
|
150
|
-
logger.success(`✅ Archive created: ${outPath}`);
|
|
151
|
-
logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
|
|
152
|
-
|
|
153
|
-
// 2. Authenticate and upload directly (+ optional coverage)
|
|
154
|
-
logger.info('2/3: Uploading to deployment service...');
|
|
155
|
-
const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
|
|
156
|
-
const uploadResult = await uploadBuild(
|
|
157
|
-
apiClient,
|
|
158
|
-
{
|
|
159
|
-
project: argv.project,
|
|
160
|
-
version: argv.version,
|
|
161
|
-
},
|
|
162
|
-
{ zipPath: outPath, coverageReport }
|
|
163
|
-
);
|
|
164
|
-
logger.success('✅ Archive uploaded.');
|
|
165
|
-
logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
|
|
166
|
-
|
|
167
|
-
await postPRComment(buildDeployResult(argv, coverageSummary, uploadResult), coverageSummary);
|
|
168
|
-
|
|
169
|
-
logger.success('\n🎉 Deployment successful! 🎉');
|
|
170
|
-
logUploadLinks(argv, coverageSummary, uploadResult, logger);
|
|
171
|
-
}
|
|
124
|
+
await postPRComment(buildDeployResult(argv, coverageSummary, uploadResult), coverageSummary);
|
|
125
|
+
|
|
126
|
+
logger.success('\n🎉 Deployment successful! 🎉');
|
|
127
|
+
logUploadLinks(argv, coverageSummary, uploadResult, logger);
|
|
172
128
|
|
|
173
129
|
} finally {
|
|
174
130
|
// 4. Clean up the local archive
|
|
@@ -176,6 +132,10 @@ async function runDeployment(argv) {
|
|
|
176
132
|
fs.unlinkSync(outPath);
|
|
177
133
|
logger.info(`🧹 Cleaned up temporary file: ${outPath}`);
|
|
178
134
|
}
|
|
135
|
+
if (metadataZipPath && fs.existsSync(metadataZipPath)) {
|
|
136
|
+
fs.unlinkSync(metadataZipPath);
|
|
137
|
+
logger.info(`🧹 Cleaned up temporary file: ${metadataZipPath}`);
|
|
138
|
+
}
|
|
179
139
|
}
|
|
180
140
|
}
|
|
181
141
|
|
|
@@ -394,7 +354,7 @@ async function main() {
|
|
|
394
354
|
}, async (argv) => {
|
|
395
355
|
const logger = createLogger(argv);
|
|
396
356
|
|
|
397
|
-
const
|
|
357
|
+
const result = await runCoverageAnalysis({
|
|
398
358
|
storybookDir: argv.dir,
|
|
399
359
|
baseBranch: argv.coverageBase || 'main',
|
|
400
360
|
failOnThreshold: Boolean(argv.coverageFailOnThreshold),
|
|
@@ -402,6 +362,7 @@ async function main() {
|
|
|
402
362
|
outputPath: argv.output,
|
|
403
363
|
keepReport: true,
|
|
404
364
|
});
|
|
365
|
+
const report = result.report;
|
|
405
366
|
|
|
406
367
|
if (!report) {
|
|
407
368
|
logger.error('Coverage: no report generated (tool failed or returned null)');
|
|
@@ -485,22 +446,32 @@ async function resolveCoverage(argv, logger) {
|
|
|
485
446
|
const enabled = argv.coverage !== false;
|
|
486
447
|
if (!enabled) {
|
|
487
448
|
logger.info('Coverage: disabled (--no-coverage)');
|
|
488
|
-
return { coverageReport: null, coverageSummary: null };
|
|
449
|
+
return { coverageReport: null, coverageSummary: null, metadataZipPath: null };
|
|
489
450
|
}
|
|
490
451
|
|
|
491
452
|
try {
|
|
492
453
|
let report = null;
|
|
454
|
+
let metadataZipPath = null;
|
|
493
455
|
|
|
494
456
|
if (argv.coverageReport) {
|
|
495
457
|
logger.info(`Coverage: using existing report at ${argv.coverageReport}`);
|
|
496
458
|
report = loadCoverageReport(argv.coverageReport);
|
|
497
459
|
} else {
|
|
498
|
-
|
|
460
|
+
const needsScreenshots = Boolean(argv.withAnalysis);
|
|
461
|
+
const outputZipPath = needsScreenshots
|
|
462
|
+
? path.join(os.tmpdir(), `scry-metadata-${Date.now()}.zip`)
|
|
463
|
+
: null;
|
|
464
|
+
|
|
465
|
+
const result = await runCoverageAnalysis({
|
|
499
466
|
storybookDir: argv.dir,
|
|
500
467
|
baseBranch: argv.coverageBase || 'main',
|
|
501
468
|
failOnThreshold: Boolean(argv.coverageFailOnThreshold),
|
|
502
|
-
execute: Boolean(argv.coverageExecute),
|
|
469
|
+
execute: Boolean(argv.coverageExecute) || needsScreenshots,
|
|
470
|
+
screenshots: needsScreenshots,
|
|
471
|
+
outputZipPath,
|
|
503
472
|
});
|
|
473
|
+
report = result.report;
|
|
474
|
+
metadataZipPath = result.metadataZipPath;
|
|
504
475
|
}
|
|
505
476
|
|
|
506
477
|
const summary = extractCoverageSummary(report);
|
|
@@ -511,7 +482,7 @@ async function resolveCoverage(argv, logger) {
|
|
|
511
482
|
logger.info('Coverage: no report generated (tool failed or report shape unexpected)');
|
|
512
483
|
}
|
|
513
484
|
|
|
514
|
-
return { coverageReport: report, coverageSummary: summary };
|
|
485
|
+
return { coverageReport: report, coverageSummary: summary, metadataZipPath };
|
|
515
486
|
} catch (err) {
|
|
516
487
|
logger.error(`Coverage: failed (${err.message})`);
|
|
517
488
|
throw err;
|
package/lib/apiClient.js
CHANGED
|
@@ -282,6 +282,50 @@ async function uploadCoverageReportDirectly(apiClient, target, coverageReport) {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Upload metadata+screenshots ZIP.
|
|
287
|
+
* This triggers async build-processing through the upload service queue.
|
|
288
|
+
*
|
|
289
|
+
* @param {axios.AxiosInstance} apiClient
|
|
290
|
+
* @param {{project: string, version: string}} target
|
|
291
|
+
* @param {string} metadataZipPath
|
|
292
|
+
* @param {{info:Function,success:Function,warn:Function}} customLogger
|
|
293
|
+
* @returns {Promise<{success:boolean, status?:number, queued?:boolean, buildNumber?:number, zipKey?:string, error?:string}>}
|
|
294
|
+
*/
|
|
295
|
+
async function uploadMetadataZip(apiClient, target, metadataZipPath, customLogger = logger) {
|
|
296
|
+
const projectName = target.project || 'main';
|
|
297
|
+
const versionName = target.version || 'latest';
|
|
298
|
+
const url = `/upload/${projectName}/${versionName}/metadata`;
|
|
299
|
+
|
|
300
|
+
customLogger.info('Uploading metadata ZIP...');
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const fileBuffer = fs.readFileSync(metadataZipPath);
|
|
304
|
+
const response = await apiClient.post(url, fileBuffer, {
|
|
305
|
+
headers: { 'Content-Type': 'application/zip' },
|
|
306
|
+
maxContentLength: 100 * 1024 * 1024,
|
|
307
|
+
maxBodyLength: 100 * 1024 * 1024,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const data = response.data || {};
|
|
311
|
+
customLogger.success(
|
|
312
|
+
`Metadata ZIP uploaded (build #${data.buildNumber ?? 'n/a'}, queued: ${Boolean(data.queued)})`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
success: true,
|
|
317
|
+
status: response.status,
|
|
318
|
+
queued: Boolean(data.queued),
|
|
319
|
+
buildNumber: data.buildNumber,
|
|
320
|
+
zipKey: data.zipKey,
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error.response?.data?.error || error.message || 'Unknown error';
|
|
324
|
+
customLogger.warn(`Metadata ZIP upload failed: ${message}`);
|
|
325
|
+
return { success: false, error: message };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
285
329
|
/**
|
|
286
330
|
* Upload storybook zip plus optional coverage report.
|
|
287
331
|
*
|
|
@@ -290,7 +334,7 @@ async function uploadCoverageReportDirectly(apiClient, target, coverageReport) {
|
|
|
290
334
|
*
|
|
291
335
|
* @param {axios.AxiosInstance} apiClient
|
|
292
336
|
* @param {{project: string, version: string}} target
|
|
293
|
-
* @param {{zipPath: string, coverageReport?: any|null}} options
|
|
337
|
+
* @param {{zipPath: string, coverageReport?: any|null, metadataZipPath?: string|null}} options
|
|
294
338
|
*/
|
|
295
339
|
async function uploadBuild(apiClient, target, options) {
|
|
296
340
|
logger.debug('uploadBuild orchestration started');
|
|
@@ -313,13 +357,19 @@ async function uploadBuild(apiClient, target, options) {
|
|
|
313
357
|
}
|
|
314
358
|
}
|
|
315
359
|
|
|
316
|
-
|
|
360
|
+
let metadataUpload = null;
|
|
361
|
+
if (options.metadataZipPath) {
|
|
362
|
+
metadataUpload = await uploadMetadataZip(apiClient, target, options.metadataZipPath, logger);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { zipUpload, coverageUpload, metadataUpload };
|
|
317
366
|
}
|
|
318
367
|
|
|
319
368
|
module.exports = {
|
|
320
369
|
getApiClient,
|
|
321
370
|
uploadFileDirectly,
|
|
322
371
|
uploadCoverageReportDirectly,
|
|
372
|
+
uploadMetadataZip,
|
|
323
373
|
uploadBuild,
|
|
324
374
|
requestPresignedUrl,
|
|
325
375
|
putToPresignedUrl,
|
package/lib/coverage.js
CHANGED
|
@@ -10,6 +10,8 @@ const chalk = require('chalk');
|
|
|
10
10
|
* @property {boolean} [failOnThreshold=false] If true, pass "--ci" to the coverage tool and rethrow errors
|
|
11
11
|
* @property {string} [outputPath] If provided, write the report to this path (relative to cwd allowed)
|
|
12
12
|
* @property {boolean} [keepReport=false] If true, do not delete the output file after reading
|
|
13
|
+
* @property {boolean} [screenshots=false] Enable passing-story screenshots in scry-sbcov
|
|
14
|
+
* @property {string|null} [outputZipPath=null] Where to write metadata+screenshots ZIP
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -21,13 +23,23 @@ const chalk = require('chalk');
|
|
|
21
23
|
* - Reads and returns the parsed JSON report
|
|
22
24
|
* - Deletes the temporary report file
|
|
23
25
|
*
|
|
24
|
-
* If the underlying tool fails and `failOnThreshold` is false, returns
|
|
26
|
+
* If the underlying tool fails and `failOnThreshold` is false, returns
|
|
27
|
+
* `{ report: null, metadataZipPath: null }`.
|
|
25
28
|
*
|
|
26
29
|
* @param {RunCoverageOptions} options
|
|
27
|
-
* @returns {Promise<any|null
|
|
30
|
+
* @returns {Promise<{report:any|null, metadataZipPath:string|null}>}
|
|
28
31
|
*/
|
|
29
32
|
async function runCoverageAnalysis(options) {
|
|
30
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
storybookDir,
|
|
35
|
+
baseBranch = 'main',
|
|
36
|
+
failOnThreshold = false,
|
|
37
|
+
execute = false,
|
|
38
|
+
outputPath: providedOutputPath,
|
|
39
|
+
keepReport = false,
|
|
40
|
+
screenshots = false,
|
|
41
|
+
outputZipPath = null,
|
|
42
|
+
} = options || {};
|
|
31
43
|
|
|
32
44
|
if (!storybookDir || typeof storybookDir !== 'string') {
|
|
33
45
|
throw new Error('runCoverageAnalysis: options.storybookDir is required');
|
|
@@ -56,13 +68,21 @@ async function runCoverageAnalysis(options) {
|
|
|
56
68
|
cliArgs.push('--ci');
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
if (execute) {
|
|
71
|
+
if (execute || screenshots) {
|
|
60
72
|
cliArgs.push('--execute');
|
|
61
73
|
}
|
|
74
|
+
if (screenshots) {
|
|
75
|
+
cliArgs.push('--screenshots');
|
|
76
|
+
if (outputZipPath) {
|
|
77
|
+
cliArgs.push('--output-zip', outputZipPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
81
|
+
// Allow local override for E2E testing before package publication.
|
|
82
|
+
// Example:
|
|
83
|
+
// SCRY_SBCOV_CMD="node /abs/path/to/scry-sbcov/dist/cli/index.js"
|
|
84
|
+
const sbcovCommandPrefix = (process.env.SCRY_SBCOV_CMD || 'npx -y @scrymore/scry-sbcov').trim();
|
|
85
|
+
const npxCommand = `${sbcovCommandPrefix} ${cliArgs.map(shellEscape).join(' ')}`;
|
|
66
86
|
|
|
67
87
|
// Debug logging to show the exact command being executed
|
|
68
88
|
console.log(chalk.yellow('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
@@ -91,13 +111,16 @@ async function runCoverageAnalysis(options) {
|
|
|
91
111
|
|
|
92
112
|
const raw = fs.readFileSync(outputPath, 'utf-8');
|
|
93
113
|
const report = JSON.parse(raw);
|
|
114
|
+
const metadataZipPath = (screenshots && outputZipPath && fs.existsSync(outputZipPath))
|
|
115
|
+
? outputZipPath
|
|
116
|
+
: null;
|
|
94
117
|
|
|
95
118
|
if (!keepReport && !providedOutputPath) safeUnlink(outputPath);
|
|
96
|
-
return report;
|
|
119
|
+
return { report, metadataZipPath };
|
|
97
120
|
} catch (error) {
|
|
98
121
|
if (!keepReport && !providedOutputPath) safeUnlink(outputPath);
|
|
99
122
|
if (failOnThreshold) throw error;
|
|
100
|
-
return null;
|
|
123
|
+
return { report: null, metadataZipPath: null };
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrymore/scry-deployer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A CLI to automate the deployment of Storybook static builds.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@octokit/rest": "^20.0.0",
|
|
40
|
-
"@scrymore/scry-sbcov": "^0.
|
|
40
|
+
"@scrymore/scry-sbcov": "^0.3.0",
|
|
41
41
|
"@sentry/node": "^10.33.0",
|
|
42
42
|
"archiver": "^7.0.1",
|
|
43
43
|
"axios": "^1.12.2",
|