@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/README.md +118 -65
- package/bin/cli.js +304 -48
- package/lib/config.js +11 -3
- package/lib/coverage.js +220 -11
- package/lib/metadataArchive.js +135 -0
- package/lib/telemetry.js +0 -67
- package/lib/templates.js +93 -50
- package/lib/update-workflows.js +8 -5
- package/lib/versionCheck.js +112 -0
- package/package.json +1 -1
- package/scripts/regenerate-workflow-templates.js +23 -0
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { initTelemetry, captureCliError, flushTelemetry
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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: '
|
|
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ā
|
|
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
|
-
|
|
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 => {
|