@scrymore/scry-deployer 0.6.1 → 0.7.0-next.20260926193255

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { initTelemetry, captureCliError, flushTelemetry, redactArgv } = require('../lib/telemetry.js');
3
+ const { initTelemetry, captureCliError, flushTelemetry } = require('../lib/telemetry.js');
4
4
  const yargs = require('yargs/yargs');
5
5
  const { hideBin } = require('yargs/helpers');
6
6
  const fs = require('fs');
@@ -21,12 +21,14 @@ const { runUpdateWorkflows } = require('../lib/update-workflows.js');
21
21
  const { runQueueImageUpload } = require('../lib/imageUpload.js');
22
22
  const { runLocalImageProcessing } = require('../lib/localImageProcessing.js');
23
23
  const { resolveBuildGitContext } = require('../lib/gitContext.js');
24
+ const { countMetadataEntries, readSbcovManifest, droppedReasons } = require('../lib/metadataArchive.js');
25
+ const { checkForNewerVersion } = require('../lib/versionCheck.js');
26
+ const { version: DEPLOYER_VERSION } = require('../package.json');
24
27
 
25
28
  async function runAnalysis(argv) {
26
29
  const logger = createLogger(argv);
27
30
  logger.info('šŸ“Š Starting Storybook analysis...');
28
- // Credentials masked: this line is also a Sentry breadcrumb.
29
- logger.debug(`Received arguments: ${JSON.stringify(redactArgv(argv))}`);
31
+ logger.debug(`Received arguments: ${JSON.stringify(argv)}`);
30
32
 
31
33
  const outPath = path.join(os.tmpdir(), `storybook-analysis-${Date.now()}.zip`);
32
34
 
@@ -83,11 +85,57 @@ async function runAnalysis(argv) {
83
85
  }
84
86
  }
85
87
 
88
+ /**
89
+ * Decide whether this deploy runs analysis (screenshots + metadata for search).
90
+ *
91
+ * On by default since 0.7.0: an unset flag used to mean "off", and a workflow
92
+ * that forgot --with-analysis uploaded, printed success and indexed nothing
93
+ * (ISSUES.md #50). Opting out is explicit, and the caller says so in the log.
94
+ *
95
+ * @param {any} argv
96
+ * @returns {{enabled:boolean, optOut:string|null}}
97
+ */
98
+ function resolveAnalysis(argv) {
99
+ if (argv.analysis === false) {
100
+ return { enabled: false, optOut: '--no-analysis' };
101
+ }
102
+ if (argv.withAnalysis === false) {
103
+ return { enabled: false, optOut: 'withAnalysis is false in .storybook-deployer.json or SCRY_WITH_ANALYSIS' };
104
+ }
105
+ if (argv.withAnalysis === true || argv.analysis === true) {
106
+ return { enabled: true, optOut: null };
107
+ }
108
+ // Analysis runs inside the coverage tool, so --no-coverage without an
109
+ // explicit analysis flag is an opt-out of both. With --with-analysis it is
110
+ // not, and ends red below (analysis asked for, nothing produced).
111
+ if (argv.coverage === false) {
112
+ return { enabled: false, optOut: '--no-coverage (analysis runs inside coverage)' };
113
+ }
114
+ // A supplied report skips the coverage run, and with it the capture.
115
+ if (argv.coverageReport) {
116
+ return { enabled: false, optOut: '--coverage-report (a supplied report has no screenshots)' };
117
+ }
118
+ return { enabled: true, optOut: null };
119
+ }
120
+
121
+ /**
122
+ * Stories the coverage report says exist, and the first capture error, for
123
+ * the "captured 0 of N" line. Both optional: the report shape is sbcov's.
124
+ */
125
+ function describeCapture(report) {
126
+ const total = report?.summary?.totalStories ?? report?.execution?.summary?.total ?? null;
127
+ const f = report?.execution?.failures?.[0];
128
+ const firstError = f ? `${f.storyId || f.storyName || 'a story'}: ${String(f.message || f.failureType || '').split('\n')[0]}` : null;
129
+ return { total: typeof total === 'number' ? total : null, firstError };
130
+ }
131
+
86
132
  async function runDeployment(argv) {
87
133
  const logger = createLogger(argv);
88
134
  logger.info('šŸš€ Starting deployment...');
89
- // Credentials masked: this line is also a Sentry breadcrumb.
90
- logger.debug(`Received arguments: ${JSON.stringify(redactArgv(argv))}`);
135
+ logger.debug(`Received arguments: ${JSON.stringify(argv)}`);
136
+
137
+ const analysis = resolveAnalysis(argv);
138
+ argv = { ...argv, withAnalysis: analysis.enabled };
91
139
 
92
140
  const outPath = path.join(os.tmpdir(), `storybook-deployment-${Date.now()}.zip`);
93
141
  let metadataZipPath = null;
@@ -96,12 +144,57 @@ async function runDeployment(argv) {
96
144
  const coverage = await resolveCoverage(argv, logger);
97
145
  const coverageReport = coverage.coverageReport;
98
146
  const coverageSummary = coverage.coverageSummary;
147
+ const sbcovFailure = coverage.sbcovFailure || null;
99
148
  metadataZipPath = coverage.metadataZipPath;
100
149
 
101
150
  if (argv.withAnalysis) {
102
151
  logger.info('Running deployment with analysis...');
103
152
  }
104
153
 
154
+ // Count what the archive holds before sending it. An archive whose
155
+ // metadata.json is [] is what sbcov writes when every story failed
156
+ // after the browser launched; queuing it produced a build marked
157
+ // `completed` with nothing in it, and a green run (ISSUES.md #50).
158
+ // Hosting still goes ahead (the preview link stays useful); the
159
+ // archive is not sent and the run ends red below.
160
+ let metadataToSend = metadataZipPath;
161
+ let emptyArchive = null;
162
+ let dropped = null;
163
+ if (coverage.maxDroppedUnsupported) {
164
+ logger.warn(
165
+ 'āš ļø The installed scry-sbcov does not support --max-dropped, so stories that fail to\n' +
166
+ ' capture are not counted by it. Upgrade @scrymore/scry-sbcov to 0.5.2 or later.'
167
+ );
168
+ }
169
+ if (metadataZipPath) {
170
+ // sbcov 0.5.2+ lists every story it could not capture in the archive.
171
+ // Read it whatever sbcov's exit code: a sbcov that ignored
172
+ // --max-dropped still names its drops, and the allowance is applied here.
173
+ const { manifest, error: manifestError } = readSbcovManifest(metadataZipPath);
174
+ if (manifestError) {
175
+ logger.warn(`āš ļø ${manifestError}; dropped stories cannot be counted for this build.`);
176
+ } else if (manifest) {
177
+ const allowed = coverage.effectiveMaxDropped ?? 0;
178
+ const n = manifest.dropped.length;
179
+ const reasons = n ? ` (${droppedReasons(manifest.dropped)})` : '';
180
+ logger.info(`scry-sbcov: ${manifest.captured ?? '?'}/${manifest.declared ?? '?'} stories captured, ${n} not captured${reasons}.`);
181
+ dropped = { n, allowed, reasons, declared: manifest.declared, captured: manifest.captured };
182
+ }
183
+ }
184
+ if (metadataZipPath) {
185
+ const counted = countMetadataEntries(metadataZipPath);
186
+ if (counted.count === 0) {
187
+ emptyArchive = { ...describeCapture(coverageReport), note: counted.error };
188
+ metadataToSend = null;
189
+ } else if (counted.count === null) {
190
+ // Could not read it. Send it anyway (the service decides) but
191
+ // say so: this is not a count of zero.
192
+ logger.warn(`āš ļø Could not count the stories in the analysis archive (${counted.error}); uploading it anyway.`);
193
+ } else {
194
+ logger.info(`Analysis archive holds ${counted.count} captured ${counted.count === 1 ? 'story' : 'stories'}.`);
195
+ }
196
+ }
197
+
105
198
  // 1. Archive only the static Storybook files.
106
199
  logger.info(`1/3: Zipping directory '${argv.dir}'...`);
107
200
  await zipDirectory(argv.dir, outPath);
@@ -130,7 +223,7 @@ async function runDeployment(argv) {
130
223
  {
131
224
  zipPath: outPath,
132
225
  coverageReport,
133
- metadataZipPath,
226
+ metadataZipPath: metadataToSend,
134
227
  gitContext,
135
228
  }
136
229
  );
@@ -148,44 +241,7 @@ async function runDeployment(argv) {
148
241
  logger.success('\nāœ… Upload complete.');
149
242
  logUploadLinks(argv, coverageSummary, uploadResult, logger);
150
243
 
151
- if (uploadResult?.metadataUpload?.queued) {
152
- logger.info(
153
- '\nā³ Indexing has been queued, not finished.\n' +
154
- ' This command cannot confirm it succeeded. Components will not be\n' +
155
- ' searchable until processing completes, and a failed build reports\n' +
156
- ' nothing here. Before relying on search, confirm the build shows\n' +
157
- " processingStatus 'completed' rather than 'failed'."
158
- );
159
- } else if (uploadResult?.metadataUpload) {
160
- logger.warn(
161
- '\nāš ļø Metadata was uploaded but not queued for processing.\n' +
162
- ' The Storybook is hosted, but its components are NOT being indexed.'
163
- );
164
- } else if (argv.withAnalysis) {
165
- // The gap between the two branches above, and the most damaging
166
- // state of the three: analysis was asked for, produced nothing, and
167
- // this command used to say "Upload successful" and stop. Nothing is
168
- // ever indexed, no error is printed, and CI stays green — so the
169
- // first sign of trouble is a customer reporting that search is empty
170
- // days later (ISSUES.md #24).
171
- //
172
- // Exit non-zero. The Storybook is hosted, so "failure" overstates it
173
- // slightly, but the job asked for was to make components searchable
174
- // and that did not happen. A red build is the only signal that gets
175
- // acted on.
176
- process.exitCode = 1;
177
- logger.error(
178
- '\nāŒ Analysis produced no metadata, so NOTHING WILL BE INDEXED.\n' +
179
- ' The Storybook is hosted and browsable, but no component will be\n' +
180
- ' searchable from this build.\n\n' +
181
- ' You asked for --with-analysis and it did not complete. The cause is\n' +
182
- ' in the coverage output above — commonly a missing Playwright browser\n' +
183
- ' (run: npx playwright install chromium-headless-shell) or a TypeScript\n' +
184
- ' resolution error in the analyzer.\n\n' +
185
- ' Exiting non-zero deliberately: a green build here would mean search\n' +
186
- ' silently returns nothing.'
187
- );
188
- }
244
+ reportIndexingOutcome({ argv, analysis, uploadResult, emptyArchive, sbcovFailure, dropped, logger });
189
245
 
190
246
  } finally {
191
247
  // 4. Clean up the local archive
@@ -200,6 +256,122 @@ async function runDeployment(argv) {
200
256
  }
201
257
  }
202
258
 
259
+ const HOSTED_NOT_SEARCHABLE =
260
+ ' The Storybook is hosted and browsable, but no component will be\n' +
261
+ ' searchable from this build.';
262
+
263
+ /**
264
+ * Say what happened to indexing, and set the exit code from it.
265
+ *
266
+ * The rule (ISSUES.md #24, #50): a deploy that was asked to index and will
267
+ * index nothing ends with exit code 1. The Storybook is hosted either way.
268
+ */
269
+ function reportIndexingOutcome({ argv, analysis, uploadResult, emptyArchive, sbcovFailure, dropped = null, logger }) {
270
+ const metadataUpload = uploadResult?.metadataUpload || null;
271
+
272
+ if (!argv.withAnalysis) {
273
+ logger.info(`\nā„¹ļø Analysis skipped (${analysis.optOut || 'not requested'}): this build is hosted but NOT searchable.`);
274
+ if (sbcovFailure) {
275
+ // Coverage is optional here; the report is what failed.
276
+ logger.warn(`āš ļø Coverage report not produced: ${sbcovFailure.reason}.`);
277
+ }
278
+ return;
279
+ }
280
+
281
+ if (emptyArchive) {
282
+ // Checked first: the archive was not sent, so whatever the upload
283
+ // result says about metadata is not about this build's stories.
284
+ process.exitCode = 1;
285
+ const of = emptyArchive.total !== null ? ` of ${emptyArchive.total}` : '';
286
+ logger.error(
287
+ `\nāŒ Analysis captured 0${of} stories, so NOTHING WILL BE INDEXED.\n` +
288
+ (emptyArchive.firstError ? ` First capture error: ${emptyArchive.firstError}\n` : '') +
289
+ (emptyArchive.note ? ` The archive: ${emptyArchive.note}.\n` : '') +
290
+ (sbcovFailure ? ` ${sbcovFailure.reason}.\n` : '') +
291
+ ' The empty archive was not uploaded and no build was queued.\n' +
292
+ HOSTED_NOT_SEARCHABLE
293
+ );
294
+ return;
295
+ }
296
+
297
+ if (metadataUpload && metadataUpload.success === false) {
298
+ // apiClient turns a rejected upload into {success:false}; the old
299
+ // check tested only that the object existed, printed "uploaded but
300
+ // not queued" and exited 0.
301
+ process.exitCode = 1;
302
+ logger.error(
303
+ `\nāŒ The metadata upload failed (${metadataUpload.error || 'no reason given'}), so NOTHING WILL BE INDEXED.\n` +
304
+ HOSTED_NOT_SEARCHABLE
305
+ );
306
+ return;
307
+ }
308
+
309
+ if (metadataUpload?.queued) {
310
+ logger.info(
311
+ '\nā³ Indexing has been queued, not finished.\n' +
312
+ ' This command cannot confirm it succeeded. Components will not be\n' +
313
+ ' searchable until processing completes, and a failed build reports\n' +
314
+ ' nothing here. Before relying on search, confirm the build shows\n' +
315
+ " processingStatus 'completed' rather than 'failed'."
316
+ );
317
+ const droppedLine = dropped && dropped.n
318
+ ? ` ${dropped.n} of ${dropped.declared ?? '?'} stories were not captured${dropped.reasons}; see sbcov-manifest.json in the archive.\n`
319
+ : '';
320
+ if (sbcovFailure) {
321
+ // The contract with scry-sbcov (#51): exit 3 = stories were dropped
322
+ // above --max-dropped, and the archive of the ones that captured
323
+ // was written. Those are queued above; the run still ends red.
324
+ process.exitCode = 1;
325
+ logger.error(
326
+ `\nāŒ ${sbcovFailure.reason}. The stories that were captured are queued for\n` +
327
+ ' indexing, but this build is incomplete: some components will be\n' +
328
+ ' missing from search. See the scry-sbcov output above.\n' +
329
+ droppedLine
330
+ );
331
+ } else if (dropped && dropped.n > dropped.allowed) {
332
+ // sbcov exited 0 but its manifest lists more drops than allowed: an
333
+ // sbcov that ignored --max-dropped, or one run without it. The
334
+ // allowance is enforced here too, so any dropped story still ends
335
+ // the deploy red (after the rest were queued) unless --max-dropped
336
+ // says otherwise.
337
+ process.exitCode = 1;
338
+ logger.error(
339
+ `\nāŒ ${dropped.n} of ${dropped.declared ?? '?'} stories were not captured${dropped.reasons}, more than\n` +
340
+ ` --max-dropped ${dropped.allowed} allows. The ${dropped.captured ?? 'other'} captured stories are queued; the rest\n` +
341
+ ' will not be searchable. See sbcov-manifest.json in the archive, or raise\n' +
342
+ ' --max-dropped (SCRY_MAX_DROPPED) to accept them.'
343
+ );
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (metadataUpload) {
349
+ process.exitCode = 1;
350
+ logger.error(
351
+ '\nāŒ Metadata was uploaded but not queued for processing, so NOTHING WILL BE INDEXED.\n' +
352
+ HOSTED_NOT_SEARCHABLE
353
+ );
354
+ return;
355
+ }
356
+
357
+ process.exitCode = 1;
358
+
359
+ // The gap between the branches above, and the most damaging state:
360
+ // analysis was asked for, produced nothing, and this command used to say
361
+ // "Upload successful" and stop (ISSUES.md #24).
362
+ logger.error(
363
+ '\nāŒ Analysis produced no metadata, so NOTHING WILL BE INDEXED.\n' +
364
+ HOSTED_NOT_SEARCHABLE + '\n\n' +
365
+ (sbcovFailure
366
+ ? ` Cause: ${sbcovFailure.reason}. See the scry-sbcov output above.\n\n`
367
+ : ' The cause is in the coverage output above — commonly a missing Playwright\n' +
368
+ " browser (install it with the deployer's own Playwright; see the README's\n" +
369
+ ' CI setup) or a TypeScript resolution error in the analyzer.\n\n') +
370
+ ' Exiting non-zero deliberately: a green build here would mean search\n' +
371
+ ' silently returns nothing. Pass --no-analysis to host without indexing.'
372
+ );
373
+ }
374
+
203
375
  async function handleError(error, argv) {
204
376
  const logger = createLogger(argv || {});
205
377
  logger.error(`\nāŒ Error: ${error.message}`);
@@ -281,10 +453,31 @@ async function main() {
281
453
  type: 'boolean',
282
454
  default: false,
283
455
  })
456
+ .option('capture-mode', {
457
+ describe: 'Screenshot framing forwarded to scry-sbcov: root or viewport (unset: sbcov default)',
458
+ type: 'string',
459
+ choices: ['root', 'viewport'],
460
+ })
461
+ .option('capture-scale', {
462
+ describe: 'Screenshot device scale factor forwarded to scry-sbcov, 0 < n <= 4 (unset: sbcov default)',
463
+ type: 'string',
464
+ })
465
+ .option('capture-viewport', {
466
+ describe: 'Browser viewport WIDTHxHEIGHT forwarded to scry-sbcov (unset: sbcov default)',
467
+ type: 'string',
468
+ })
284
469
  .option('with-analysis', {
285
- describe: 'Include Storybook analysis (screenshots, metadata)',
470
+ describe: 'Capture screenshots and metadata so components are searchable (the default since 0.7.0)',
471
+ type: 'boolean',
472
+ })
473
+ .option('analysis', {
474
+ describe: 'Pass --no-analysis to host the Storybook without indexing it (nothing will be searchable)',
286
475
  type: 'boolean',
287
476
  })
477
+ .option('max-dropped', {
478
+ describe: 'End red (after uploading the rest) when more than this many stories fail to capture. Default 0: any dropped story ends the deploy red. Forwarded to scry-sbcov',
479
+ type: 'string',
480
+ })
288
481
  .option('storybook-url', {
289
482
  describe: 'URL of the Storybook for screenshot capture',
290
483
  type: 'string',
@@ -318,6 +511,9 @@ async function main() {
318
511
  throw new Error(`Path is not a directory: ${config.dir}`);
319
512
  }
320
513
 
514
+ // Advisory, bounded to 2 s, never fails the deploy.
515
+ await checkForNewerVersion({ currentVersion: DEPLOYER_VERSION, logger: createLogger(config) });
516
+
321
517
  await runDeployment(config);
322
518
  })
323
519
  .command('analyze', 'Analyze Storybook stories and generate metadata', (yargs) => {
@@ -389,6 +585,23 @@ async function main() {
389
585
  default: false,
390
586
  alias: 'coverageExecute'
391
587
  })
588
+ .option('capture-mode', {
589
+ describe: 'Screenshot framing forwarded to scry-sbcov: root or viewport (unset: sbcov default)',
590
+ type: 'string',
591
+ choices: ['root', 'viewport'],
592
+ })
593
+ .option('capture-scale', {
594
+ describe: 'Screenshot device scale factor forwarded to scry-sbcov, 0 < n <= 4 (unset: sbcov default)',
595
+ type: 'string',
596
+ })
597
+ .option('capture-viewport', {
598
+ describe: 'Browser viewport WIDTHxHEIGHT forwarded to scry-sbcov (unset: sbcov default)',
599
+ type: 'string',
600
+ })
601
+ .option('max-dropped', {
602
+ describe: 'Forwarded to scry-sbcov: fail when more than this many stories fail to capture',
603
+ type: 'string',
604
+ })
392
605
  .option('output', {
393
606
  describe: 'Where to write the JSON coverage report',
394
607
  type: 'string',
@@ -402,6 +615,8 @@ async function main() {
402
615
  }, async (argv) => {
403
616
  const logger = createLogger(argv);
404
617
 
618
+ // Capture settings: CLI flag > env > .storybook-deployer.json; unset = sbcov decides.
619
+ const { captureMode, captureScale, captureViewport, maxDropped } = loadConfig(argv);
405
620
  const result = await runCoverageAnalysis({
406
621
  storybookDir: argv.dir,
407
622
  baseBranch: argv.coverageBase || 'main',
@@ -409,9 +624,17 @@ async function main() {
409
624
  execute: Boolean(argv.coverageExecute),
410
625
  outputPath: argv.output,
411
626
  keepReport: true,
627
+ captureMode,
628
+ captureScale,
629
+ captureViewport,
630
+ maxDropped,
412
631
  });
413
632
  const report = result.report;
414
633
 
634
+ if (result.sbcovFailure) {
635
+ logger.error(`Coverage: ${result.sbcovFailure.reason}${report ? ` (report written to ${argv.output})` : ''}`);
636
+ process.exit(1);
637
+ }
415
638
  if (!report) {
416
639
  logger.error('Coverage: no report generated (tool failed or returned null)');
417
640
  process.exit(1);
@@ -474,6 +697,27 @@ async function main() {
474
697
 
475
698
  await runInit(initConfig);
476
699
  })
700
+ .command('update-workflows', 'Regenerate the Scry GitHub Actions workflows in .github/workflows from this version\'s templates (no API key needed)', (yargs) => {
701
+ return yargs
702
+ .option('commit', {
703
+ describe: 'git add and commit the regenerated workflow files',
704
+ type: 'boolean',
705
+ default: false,
706
+ })
707
+ .option('commit-message', {
708
+ describe: 'Commit message used with --commit',
709
+ type: 'string',
710
+ default: 'chore: update Scry workflows',
711
+ alias: 'commitMessage',
712
+ })
713
+ .option('verbose', {
714
+ describe: 'Enable verbose logging',
715
+ type: 'boolean',
716
+ default: false,
717
+ });
718
+ }, async (argv) => {
719
+ await runUpdateWorkflows(argv);
720
+ })
477
721
  .command('upload-images', 'Upload a folder of images for search indexing', (yargs) => {
478
722
  return yargs
479
723
  .option('dir', {
@@ -589,12 +833,15 @@ async function resolveCoverage(argv, logger) {
589
833
  const enabled = argv.coverage !== false;
590
834
  if (!enabled) {
591
835
  logger.info('Coverage: disabled (--no-coverage)');
592
- return { coverageReport: null, coverageSummary: null, metadataZipPath: null };
836
+ return { coverageReport: null, coverageSummary: null, metadataZipPath: null, sbcovFailure: null, effectiveMaxDropped: null, maxDroppedUnsupported: false };
593
837
  }
594
838
 
595
839
  try {
596
840
  let report = null;
597
841
  let metadataZipPath = null;
842
+ let sbcovFailure = null;
843
+ let effectiveMaxDropped = null;
844
+ let maxDroppedUnsupported = false;
598
845
 
599
846
  if (argv.coverageReport) {
600
847
  logger.info(`Coverage: using existing report at ${argv.coverageReport}`);
@@ -612,9 +859,16 @@ async function resolveCoverage(argv, logger) {
612
859
  execute: Boolean(argv.coverageExecute) || needsScreenshots,
613
860
  screenshots: needsScreenshots,
614
861
  outputZipPath,
862
+ captureMode: argv.captureMode,
863
+ captureScale: argv.captureScale,
864
+ captureViewport: argv.captureViewport,
865
+ maxDropped: argv.maxDropped,
615
866
  });
616
867
  report = result.report;
617
868
  metadataZipPath = result.metadataZipPath;
869
+ sbcovFailure = result.sbcovFailure || null;
870
+ effectiveMaxDropped = result.effectiveMaxDropped ?? null;
871
+ maxDroppedUnsupported = Boolean(result.maxDroppedUnsupported);
618
872
  }
619
873
 
620
874
  const summary = extractCoverageSummary(report);
@@ -625,7 +879,7 @@ async function resolveCoverage(argv, logger) {
625
879
  logger.info('Coverage: no report generated (tool failed or report shape unexpected)');
626
880
  }
627
881
 
628
- return { coverageReport: report, coverageSummary: summary, metadataZipPath };
882
+ return { coverageReport: report, coverageSummary: summary, metadataZipPath, sbcovFailure, effectiveMaxDropped, maxDroppedUnsupported };
629
883
  } catch (err) {
630
884
  logger.error(`Coverage: failed (${err.message})`);
631
885
  throw err;
@@ -662,7 +916,7 @@ function buildDeployResult(argv, coverageSummary, uploadResult) {
662
916
  function logUploadLinks(argv, coverageSummary, uploadResult, logger) {
663
917
  const deployResult = buildDeployResult(argv, coverageSummary, uploadResult);
664
918
 
665
- logger.success('\nāœ… Upload successful!\n');
919
+ logger.success('\nāœ… Storybook hosted.\n');
666
920
  logger.info(`šŸ“– Storybook: ${deployResult.viewUrl}`);
667
921
  if (deployResult.coverageUrl) {
668
922
  logger.info(`šŸ“Š Coverage: ${deployResult.coverageUrl}`);
@@ -682,6 +936,8 @@ module.exports = {
682
936
  runDeployment,
683
937
  runAnalysis,
684
938
  resolveCoverage,
939
+ resolveAnalysis,
940
+ reportIndexingOutcome,
685
941
  buildDeployResult,
686
942
  logUploadLinks,
687
943
  };
package/lib/config.js CHANGED
@@ -14,8 +14,10 @@ const DEFAULT_CONFIG = {
14
14
  // Project and version for deployment
15
15
  project: '',
16
16
  version: '',
17
- // Analysis options
18
- withAnalysis: false,
17
+ // Analysis options. withAnalysis is deliberately absent: unset means ON
18
+ // since 0.7.0 (bin/cli.js resolveAnalysis). A default of false here was how
19
+ // a workflow that forgot --with-analysis hosted a build that indexed
20
+ // nothing and ended green (ISSUES.md #50). Opt out with --no-analysis.
19
21
  storiesDir: null, // null enables auto-detection of .stories.* files
20
22
  screenshotsDir: './__screenshots__',
21
23
  storybookUrl: '',
@@ -119,7 +121,13 @@ function loadEnvConfig() {
119
121
  'WITH_ANALYSIS': 'withAnalysis',
120
122
  'STORIES_DIR': 'storiesDir',
121
123
  'SCREENSHOTS_DIR': 'screenshotsDir',
122
- 'STORYBOOK_URL': 'storybookUrl'
124
+ 'STORYBOOK_URL': 'storybookUrl',
125
+ // Screenshot capture settings forwarded to scry-sbcov (unset = sbcov decides)
126
+ 'CAPTURE_MODE': 'captureMode',
127
+ 'CAPTURE_SCALE': 'captureScale',
128
+ 'CAPTURE_VIEWPORT': 'captureViewport',
129
+ // Forwarded to scry-sbcov as --max-dropped
130
+ 'MAX_DROPPED': 'maxDropped'
123
131
  };
124
132
 
125
133
  Object.keys(envMapping).forEach(envKey => {