@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 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 { coverageReport, coverageSummary } = await resolveCoverage(argv, logger);
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
- // 1. Capture screenshots if storybook URL provided
97
- if (argv.storybookUrl) {
98
- logger.info(`1/5: Capturing screenshots from '${argv.storybookUrl}'...`);
99
- await captureScreenshots(argv.storybookUrl, argv.storycapOptions || {});
100
- logger.success('✅ Screenshots captured');
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
- // 2. Analyze stories and map screenshots
106
- logger.info('2/5: Analyzing stories and mapping screenshots...');
107
- const analysisResults = analyzeStorybook({
108
- storiesDir: argv.storiesDir,
109
- screenshotsDir: argv.screenshotsDir,
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
- logger.success(`✅ Found ${analysisResults.summary.totalStories} stories (${analysisResults.summary.withScreenshots} with screenshots)`);
114
-
115
- // 3. Create master ZIP with staticsite, images, and metadata
116
- logger.info('3/5: Creating master archive with static site, images, and metadata...');
117
- await createMasterZip({
118
- outPath: outPath,
119
- staticsiteDir: argv.dir,
120
- screenshotsDir: argv.screenshotsDir,
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
- } else {
146
- // Simple deployment without analysis
147
- // 1. Archive the directory
148
- logger.info(`1/3: Zipping directory '${argv.dir}'...`);
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 report = await runCoverageAnalysis({
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
- report = await runCoverageAnalysis({
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
- return { zipUpload, coverageUpload };
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 `null`.
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>} The full coverage report JSON, or null if skipped/failed (non-fatal)
30
+ * @returns {Promise<{report:any|null, metadataZipPath:string|null}>}
28
31
  */
29
32
  async function runCoverageAnalysis(options) {
30
- const { storybookDir, baseBranch = 'main', failOnThreshold = false, execute = false, outputPath: providedOutputPath, keepReport = false } = options || {};
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
- // Use npx with the package name directly.
64
- // This is more reliable than `npx -p` which can fail to find the binary in some environments.
65
- const npxCommand = `npx -y @scrymore/scry-sbcov ${cliArgs.map(shellEscape).join(' ')}`;
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.0",
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.2.2",
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",