@scrymore/scry-deployer 0.9.0 → 0.9.1-next.20260927110947
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 +24 -6
- package/lib/apiClient.js +110 -20
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -400,7 +400,15 @@ async function runDeployment(argv) {
|
|
|
400
400
|
}
|
|
401
401
|
);
|
|
402
402
|
const uploadMs = uploadTimer.stop();
|
|
403
|
-
|
|
403
|
+
// A metadata upload that failed is the essential part of this deploy
|
|
404
|
+
// failing: no success line may follow it (RCA 2: the log printed
|
|
405
|
+
// "Archive uploaded" and "Upload complete" after the timeout).
|
|
406
|
+
const metadataFailed = uploadResult?.metadataUpload?.success === false;
|
|
407
|
+
if (metadataFailed) {
|
|
408
|
+
logger.info('Storybook ZIP uploaded; the metadata upload failed (see above and below).');
|
|
409
|
+
} else {
|
|
410
|
+
logger.success('✅ Archive uploaded.');
|
|
411
|
+
}
|
|
404
412
|
logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
|
|
405
413
|
|
|
406
414
|
await postPRComment(buildDeployResult(argv, coverageSummary, uploadResult), coverageSummary);
|
|
@@ -414,8 +422,8 @@ async function runDeployment(argv) {
|
|
|
414
422
|
// credential — and this command previously printed
|
|
415
423
|
// "Deployment successful" over it, sending people looking for the
|
|
416
424
|
// cause three steps downstream (ISSUES.md #4).
|
|
417
|
-
logger.success('\n✅ Upload complete.');
|
|
418
|
-
logUploadLinks(argv, coverageSummary, uploadResult, logger);
|
|
425
|
+
if (!metadataFailed) logger.success('\n✅ Upload complete.');
|
|
426
|
+
logUploadLinks(argv, coverageSummary, uploadResult, logger, { indexed: !metadataFailed });
|
|
419
427
|
|
|
420
428
|
reportIndexingOutcome({ argv, analysis, uploadResult, emptyArchive, sbcovFailure, dropped, logger });
|
|
421
429
|
|
|
@@ -475,9 +483,15 @@ function reportIndexingOutcome({ argv, analysis, uploadResult, emptyArchive, sbc
|
|
|
475
483
|
// check tested only that the object existed, printed "uploaded but
|
|
476
484
|
// not queued" and exited 0.
|
|
477
485
|
process.exitCode = 1;
|
|
486
|
+
const zip = uploadResult?.zipUpload || {};
|
|
487
|
+
const build = zip.buildId
|
|
488
|
+
? `Build ${zip.buildId}${zip.buildNumber !== undefined ? ` (#${zip.buildNumber})` : ''}`
|
|
489
|
+
: 'The build';
|
|
478
490
|
logger.error(
|
|
479
491
|
`\n❌ The metadata upload failed (${metadataUpload.error || 'no reason given'}), so NOTHING WILL BE INDEXED.\n` +
|
|
480
|
-
HOSTED_NOT_SEARCHABLE
|
|
492
|
+
HOSTED_NOT_SEARCHABLE + '\n' +
|
|
493
|
+
` ${build} is left pending: the upload service has no way to mark it failed,\n` +
|
|
494
|
+
' so it will not show as failed. Re-run this job to index it.'
|
|
481
495
|
);
|
|
482
496
|
return;
|
|
483
497
|
}
|
|
@@ -1102,10 +1116,14 @@ function buildDeployResult(argv, coverageSummary, uploadResult) {
|
|
|
1102
1116
|
};
|
|
1103
1117
|
}
|
|
1104
1118
|
|
|
1105
|
-
function logUploadLinks(argv, coverageSummary, uploadResult, logger) {
|
|
1119
|
+
function logUploadLinks(argv, coverageSummary, uploadResult, logger, { indexed = true } = {}) {
|
|
1106
1120
|
const deployResult = buildDeployResult(argv, coverageSummary, uploadResult);
|
|
1107
1121
|
|
|
1108
|
-
|
|
1122
|
+
if (indexed) {
|
|
1123
|
+
logger.success('\n✅ Storybook hosted.\n');
|
|
1124
|
+
} else {
|
|
1125
|
+
logger.info('\nStorybook hosted (browsable, NOT indexed):\n');
|
|
1126
|
+
}
|
|
1109
1127
|
logger.info(`📖 Storybook: ${deployResult.viewUrl}`);
|
|
1110
1128
|
if (deployResult.coverageUrl) {
|
|
1111
1129
|
logger.info(`📊 Coverage: ${deployResult.coverageUrl}`);
|
package/lib/apiClient.js
CHANGED
|
@@ -22,6 +22,12 @@ const COVERAGE_RETRY_DELAY_MS = 60000;
|
|
|
22
22
|
const UPLOAD_MAX_ATTEMPTS = 4;
|
|
23
23
|
const UPLOAD_BACKOFF_BASE_MS = 2000;
|
|
24
24
|
|
|
25
|
+
// Bodies POSTed through the upload service (metadata ZIP, coverage JSON).
|
|
26
|
+
const SERVICE_UPLOAD_MAX_ATTEMPTS = 3;
|
|
27
|
+
const UPLOAD_TIMEOUT_FLOOR_MS = 60000;
|
|
28
|
+
const UPLOAD_MIN_BYTES_PER_S = 100 * 1024;
|
|
29
|
+
const UPLOAD_TIMEOUT_CAP_MS = 15 * 60 * 1000;
|
|
30
|
+
|
|
25
31
|
// Network-level failures that a later attempt can plausibly survive. Flapping
|
|
26
32
|
// DNS reports EAI_AGAIN (and, mid-flap, ENOTFOUND) rather than a clean refusal.
|
|
27
33
|
const TRANSIENT_CODES = new Set([
|
|
@@ -93,6 +99,80 @@ async function withUploadRetry(attemptFn, label) {
|
|
|
93
99
|
throw lastError;
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* How long one upload through the upload service may take: max(60 s,
|
|
104
|
+
* bytes / 100 KB/s), capped at 15 min (RCA 2 of storybook-preview-ci-runtime).
|
|
105
|
+
* A fixed 60 s failed a 28 MB metadata ZIP on a 0.4-0.85 MB/s runner uplink.
|
|
106
|
+
* SCRY_UPLOAD_TIMEOUT_FLOOR_MS lowers or raises the 60 s floor (tests, very
|
|
107
|
+
* slow links).
|
|
108
|
+
*
|
|
109
|
+
* @param {number} bytes
|
|
110
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
111
|
+
* @returns {number} milliseconds
|
|
112
|
+
*/
|
|
113
|
+
function uploadTimeoutMs(bytes, env = process.env) {
|
|
114
|
+
const floorRaw = Number(env.SCRY_UPLOAD_TIMEOUT_FLOOR_MS);
|
|
115
|
+
const floor = Number.isFinite(floorRaw) && floorRaw > 0 ? floorRaw : UPLOAD_TIMEOUT_FLOOR_MS;
|
|
116
|
+
const scaled = Math.ceil((Math.max(0, bytes) / UPLOAD_MIN_BYTES_PER_S) * 1000);
|
|
117
|
+
return Math.min(UPLOAD_TIMEOUT_CAP_MS, Math.max(floor, scaled));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatBytes(n) {
|
|
121
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
122
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
123
|
+
return `${n} B`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function describeUploadError(error) {
|
|
127
|
+
if (error?.response?.status) return `HTTP ${error.response.status}`;
|
|
128
|
+
if (error?.code === 'ECONNABORTED' || /timeout/i.test(String(error?.message))) return `timeout: ${error.message}`;
|
|
129
|
+
return error?.code || error?.message || 'unknown error';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* POST a body through the upload service with a size-scaled timeout, retrying
|
|
134
|
+
* timeouts, 5xx/429 and network errors (never other 4xx) up to 3 attempts
|
|
135
|
+
* with backoff. Every attempt is logged with its size and elapsed time, so a
|
|
136
|
+
* slow link reads as slow, not hung. Throws the last error.
|
|
137
|
+
*
|
|
138
|
+
* @param {axios.AxiosInstance} apiClient
|
|
139
|
+
* @param {string} url
|
|
140
|
+
* @param {Buffer|string|object} body
|
|
141
|
+
* @param {{label:string, bytes:number, headers:object, log?:{info:Function}}} o
|
|
142
|
+
*/
|
|
143
|
+
async function postThroughService(apiClient, url, body, { label, bytes, headers, log = logger }) {
|
|
144
|
+
const timeout = uploadTimeoutMs(bytes);
|
|
145
|
+
const backoffRaw = Number(process.env.SCRY_UPLOAD_BACKOFF_MS);
|
|
146
|
+
const backoffBase = Number.isFinite(backoffRaw) && backoffRaw >= 0 ? backoffRaw : UPLOAD_BACKOFF_BASE_MS;
|
|
147
|
+
const size = formatBytes(bytes);
|
|
148
|
+
let lastError;
|
|
149
|
+
for (let attempt = 1; attempt <= SERVICE_UPLOAD_MAX_ATTEMPTS; attempt++) {
|
|
150
|
+
const started = Date.now();
|
|
151
|
+
log.info(`${label}: attempt ${attempt}/${SERVICE_UPLOAD_MAX_ATTEMPTS}, ${size}, timeout ${Math.round(timeout / 1000)} s...`);
|
|
152
|
+
try {
|
|
153
|
+
const response = await apiClient.post(url, body, {
|
|
154
|
+
headers,
|
|
155
|
+
timeout,
|
|
156
|
+
maxContentLength: 100 * 1024 * 1024,
|
|
157
|
+
maxBodyLength: 100 * 1024 * 1024,
|
|
158
|
+
});
|
|
159
|
+
log.info(`${label}: sent ${size} in ${((Date.now() - started) / 1000).toFixed(1)} s (attempt ${attempt}/${SERVICE_UPLOAD_MAX_ATTEMPTS}).`);
|
|
160
|
+
return response;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
lastError = error;
|
|
163
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
|
164
|
+
if (!isTransientUploadError(error) || attempt === SERVICE_UPLOAD_MAX_ATTEMPTS) {
|
|
165
|
+
log.info(`${label}: attempt ${attempt}/${SERVICE_UPLOAD_MAX_ATTEMPTS} (${size}) failed after ${elapsed} s (${describeUploadError(error)}).`);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const backoff = backoffBase * Math.pow(2, attempt - 1);
|
|
169
|
+
log.info(`${label}: attempt ${attempt}/${SERVICE_UPLOAD_MAX_ATTEMPTS} (${size}) failed after ${elapsed} s (${describeUploadError(error)}); retrying in ${backoff / 1000} s...`);
|
|
170
|
+
await sleep(backoff);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw lastError;
|
|
174
|
+
}
|
|
175
|
+
|
|
96
176
|
/**
|
|
97
177
|
* Creates a pre-configured axios instance for making API calls.
|
|
98
178
|
* @param {string} apiUrl The base URL of the API.
|
|
@@ -329,14 +409,12 @@ async function uploadCoverageReportDirectly(apiClient, target, coverageReport) {
|
|
|
329
409
|
logger.debug(`Uploading coverage report for ${projectName}/${versionName}`);
|
|
330
410
|
|
|
331
411
|
try {
|
|
332
|
-
const
|
|
412
|
+
const json = JSON.stringify(coverageReport);
|
|
413
|
+
const response = await postThroughService(
|
|
414
|
+
apiClient,
|
|
333
415
|
`/upload/${projectName}/${versionName}/coverage`,
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
headers: {
|
|
337
|
-
'Content-Type': 'application/json',
|
|
338
|
-
},
|
|
339
|
-
}
|
|
416
|
+
json,
|
|
417
|
+
{ label: 'Coverage report', bytes: Buffer.byteLength(json), headers: { 'Content-Type': 'application/json' } },
|
|
340
418
|
);
|
|
341
419
|
|
|
342
420
|
logger.debug(`Coverage upload response status: ${response.status}`);
|
|
@@ -402,10 +480,11 @@ async function uploadMetadataZip(apiClient, target, metadataZipPath, customLogge
|
|
|
402
480
|
|
|
403
481
|
try {
|
|
404
482
|
const fileBuffer = fs.readFileSync(metadataZipPath);
|
|
405
|
-
const response = await apiClient
|
|
483
|
+
const response = await postThroughService(apiClient, url, fileBuffer, {
|
|
484
|
+
label: 'Metadata ZIP',
|
|
485
|
+
bytes: fileBuffer.length,
|
|
406
486
|
headers: { 'Content-Type': 'application/zip' },
|
|
407
|
-
|
|
408
|
-
maxBodyLength: 100 * 1024 * 1024,
|
|
487
|
+
log: customLogger,
|
|
409
488
|
});
|
|
410
489
|
|
|
411
490
|
const data = response.data || {};
|
|
@@ -458,19 +537,27 @@ async function uploadBuild(apiClient, target, options) {
|
|
|
458
537
|
// failure in the optional artifact was silently taking down the essential
|
|
459
538
|
// one. Observed on a 467-story library: every story captured, nothing
|
|
460
539
|
// indexed, twice.
|
|
540
|
+
// Timeouts, 5xx and network errors are retried inside the upload. The
|
|
541
|
+
// one 4xx worth waiting for is the build record not existing yet (the
|
|
542
|
+
// presigned call creates it), so only that gets the old 60 s second try.
|
|
543
|
+
const coverageFailed = (err) => logger.warn(
|
|
544
|
+
`⚠️ Coverage report upload failed: ${err.message}\n` +
|
|
545
|
+
' Continuing — this affects the coverage report only. Screenshots and\n' +
|
|
546
|
+
' metadata still upload, so components will still be indexed.'
|
|
547
|
+
);
|
|
461
548
|
try {
|
|
462
549
|
coverageUpload = await uploadCoverageReportDirectly(apiClient, target, options.coverageReport);
|
|
463
550
|
} catch (error) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
);
|
|
551
|
+
if (error.statusCode === 404 && /Build not found/.test(error.message)) {
|
|
552
|
+
logger.info('Coverage upload found no build yet; retrying in 60s...');
|
|
553
|
+
await sleep(COVERAGE_RETRY_DELAY_MS);
|
|
554
|
+
try {
|
|
555
|
+
coverageUpload = await uploadCoverageReportDirectly(apiClient, target, options.coverageReport);
|
|
556
|
+
} catch (retryError) {
|
|
557
|
+
coverageFailed(retryError);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
coverageFailed(error);
|
|
474
561
|
}
|
|
475
562
|
}
|
|
476
563
|
}
|
|
@@ -502,5 +589,8 @@ module.exports = {
|
|
|
502
589
|
putToPresignedUrl,
|
|
503
590
|
isTransientUploadError,
|
|
504
591
|
withUploadRetry,
|
|
592
|
+
uploadTimeoutMs,
|
|
593
|
+
postThroughService,
|
|
505
594
|
UPLOAD_MAX_ATTEMPTS,
|
|
595
|
+
SERVICE_UPLOAD_MAX_ATTEMPTS,
|
|
506
596
|
};
|