screenci 0.0.90 → 0.0.91
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/dist/cli.d.ts +39 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +357 -75
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +29 -29
- package/dist/docs/manifest.js +9 -9
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/camera-and-zooming.screenci.js +2 -9
- package/dist/docs/video-sources/camera-and-zooming.screenci.js.map +1 -1
- package/dist/docs/video-sources/editor.screenci.d.ts +2 -0
- package/dist/docs/video-sources/editor.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/{studio.screenci.js → editor.screenci.js} +18 -19
- package/dist/docs/video-sources/editor.screenci.js.map +1 -0
- package/dist/docs/video-sources/languages.screenci.js +12 -12
- package/dist/docs/video-sources/languages.screenci.js.map +1 -1
- package/dist/docs/video-sources/narration.screenci.js +11 -11
- package/dist/docs/video-sources/narration.screenci.js.map +1 -1
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.js +2 -2
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.js.map +1 -1
- package/dist/docs/videos.d.ts +2 -2
- package/dist/docs/videos.js +2 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/src/anonSession.d.ts +20 -6
- package/dist/src/anonSession.d.ts.map +1 -1
- package/dist/src/anonSession.js +60 -13
- package/dist/src/anonSession.js.map +1 -1
- package/dist/src/asset.d.ts +18 -15
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +18 -11
- package/dist/src/asset.js.map +1 -1
- package/dist/src/audio.d.ts +1 -1
- package/dist/src/audio.js +1 -1
- package/dist/src/builder.d.ts +25 -25
- package/dist/src/builder.d.ts.map +1 -1
- package/dist/src/builder.js +8 -8
- package/dist/src/builder.js.map +1 -1
- package/dist/src/clientOverlay.d.ts.map +1 -1
- package/dist/src/clientOverlay.js +3 -2
- package/dist/src/clientOverlay.js.map +1 -1
- package/dist/src/cue.d.ts +9 -9
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +6 -6
- package/dist/src/cue.js.map +1 -1
- package/dist/src/declare.d.ts +6 -6
- package/dist/src/declare.d.ts.map +1 -1
- package/dist/src/declare.js +9 -9
- package/dist/src/declare.js.map +1 -1
- package/dist/src/events.d.ts +8 -42
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +14 -24
- package/dist/src/events.js.map +1 -1
- package/dist/src/init.d.ts +2 -1
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +94 -143
- package/dist/src/init.js.map +1 -1
- package/dist/src/instrument.d.ts.map +1 -1
- package/dist/src/instrument.js +146 -0
- package/dist/src/instrument.js.map +1 -1
- package/dist/src/localize.d.ts +17 -11
- package/dist/src/localize.d.ts.map +1 -1
- package/dist/src/localize.js +1 -1
- package/dist/src/localize.js.map +1 -1
- package/dist/src/localizeRuntime.d.ts +8 -8
- package/dist/src/localizeRuntime.d.ts.map +1 -1
- package/dist/src/localizeRuntime.js +4 -4
- package/dist/src/localizeRuntime.js.map +1 -1
- package/dist/src/manualZoom.d.ts.map +1 -1
- package/dist/src/manualZoom.js +8 -3
- package/dist/src/manualZoom.js.map +1 -1
- package/dist/src/recordingData.d.ts +2 -2
- package/dist/src/runtimeContext.d.ts +0 -11
- package/dist/src/runtimeContext.d.ts.map +1 -1
- package/dist/src/runtimeContext.js +0 -7
- package/dist/src/runtimeContext.js.map +1 -1
- package/dist/src/screenshot.d.ts +7 -7
- package/dist/src/screenshot.d.ts.map +1 -1
- package/dist/src/studio.d.ts +41 -38
- package/dist/src/studio.d.ts.map +1 -1
- package/dist/src/studio.js +9 -9
- package/dist/src/studio.js.map +1 -1
- package/dist/src/types.d.ts +19 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/video.d.ts +15 -15
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +10 -10
- package/dist/src/video.js.map +1 -1
- package/dist/src/voiceConfig.d.ts +26 -7
- package/dist/src/voiceConfig.d.ts.map +1 -1
- package/dist/src/voices.d.ts +22 -2
- package/dist/src/voices.d.ts.map +1 -1
- package/dist/src/voices.js +18 -0
- package/dist/src/voices.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +1 -1
- package/skills/screenci/references/init.md +3 -3
- package/skills/screenci/references/record.md +1 -1
- package/dist/docs/video-sources/studio.screenci.d.ts +0 -2
- package/dist/docs/video-sources/studio.screenci.d.ts.map +0 -1
- package/dist/docs/video-sources/studio.screenci.js.map +0 -1
- package/dist/src/recordingSize.d.ts +0 -32
- package/dist/src/recordingSize.d.ts.map +0 -1
- package/dist/src/recordingSize.js +0 -54
- package/dist/src/recordingSize.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createReadStream } from 'fs';
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, } from 'fs';
|
|
4
4
|
import { createHash, randomUUID } from 'crypto';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
|
-
import { appendFile, readdir, readFile, stat, writeFile } from 'fs/promises';
|
|
6
|
+
import { appendFile, mkdir, readdir, readFile, rm, stat, writeFile, } from 'fs/promises';
|
|
7
7
|
import { delimiter, dirname, isAbsolute, relative as pathRelative, resolve, } from 'path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { Command, CommanderError } from 'commander';
|
|
@@ -17,7 +17,7 @@ import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleV
|
|
|
17
17
|
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getScreenCISecretsUrl, persistScreenCISecret, } from './src/linkSession.js';
|
|
18
18
|
import { OVERLAY_CACHE_DIR_NAME } from './src/htmlRasterizer.js';
|
|
19
19
|
import { maybeExtractVoiceSampleAudio } from './src/voiceSampleAudio.js';
|
|
20
|
-
import { ANON_SESSION_FILE, anonCredential, checkAnonSessionStatus, deleteAnonSessionFile, evaluateAnonRecordingGate, formatAnonTermsNotice, getOrCreateAnonToken,
|
|
20
|
+
import { ANON_MAX_VIDEOS_PER_RECORDING, ANON_SESSION_FILE, ANON_TOKEN_HEADER, anonCredential, checkAnonSessionStatus, deleteAnonSessionFile, evaluateAnonRecordingGate, formatAnonPostRecordNotice, formatAnonTermsNotice, getOrCreateAnonToken, readAnonSessionRecordUrl, saveAnonSessionRecordUrl, secretCredential, } from './src/anonSession.js';
|
|
21
21
|
// Re-export the environment-aware URL helpers so existing importers (and tests)
|
|
22
22
|
// can keep importing them from the CLI entrypoint.
|
|
23
23
|
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
@@ -31,6 +31,8 @@ const SCREENCI_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#scree
|
|
|
31
31
|
// Records the recordId of the most recent `screenci record` upload so
|
|
32
32
|
// `screenci info` can report exactly the run that was just made.
|
|
33
33
|
const SCREENCI_LAST_RECORD_FILE = 'last-record.json';
|
|
34
|
+
const SCREENCI_RECORD_LOCK_FILE = '.record.lock';
|
|
35
|
+
const SCREENCI_RECORD_LOCK_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
34
36
|
const require = createRequire(import.meta.url);
|
|
35
37
|
/**
|
|
36
38
|
* Reports whether the current session can complete an interactive browser
|
|
@@ -228,9 +230,9 @@ const UPLOAD_ASSET_LABEL = {
|
|
|
228
230
|
cursor: 'Cursor',
|
|
229
231
|
};
|
|
230
232
|
export function formatStudioUrl(appUrl, projectId, videoId) {
|
|
231
|
-
// The video hub resolves `?
|
|
232
|
-
// to
|
|
233
|
-
return `${appUrl}/project/${projectId}/video/${videoId}?
|
|
233
|
+
// The video hub resolves `?editor` to the right language page and scrolls it
|
|
234
|
+
// to Editor, so we never need to guess the language in the printed link.
|
|
235
|
+
return `${appUrl}/project/${projectId}/video/${videoId}?editor`;
|
|
234
236
|
}
|
|
235
237
|
class UploadAssetError extends Error {
|
|
236
238
|
constructor(message) {
|
|
@@ -350,12 +352,37 @@ async function loadUploadCandidate(screenciDir, entry, verbose) {
|
|
|
350
352
|
return {
|
|
351
353
|
entry,
|
|
352
354
|
videoName,
|
|
355
|
+
displayVideoName: videoName,
|
|
353
356
|
data,
|
|
354
357
|
preparedUploadAssets,
|
|
355
358
|
};
|
|
356
359
|
}
|
|
360
|
+
function disambiguateUploadCandidateDisplayNames(candidates) {
|
|
361
|
+
const counts = new Map();
|
|
362
|
+
for (const candidate of candidates) {
|
|
363
|
+
counts.set(candidate.videoName, (counts.get(candidate.videoName) ?? 0) + 1);
|
|
364
|
+
}
|
|
365
|
+
return candidates.map((candidate) => {
|
|
366
|
+
if ((counts.get(candidate.videoName) ?? 0) <= 1)
|
|
367
|
+
return candidate;
|
|
368
|
+
const languages = candidate.data.metadata?.languages;
|
|
369
|
+
if (Array.isArray(languages) && languages.length === 1) {
|
|
370
|
+
const [language] = languages;
|
|
371
|
+
if (typeof language === 'string' && language.length > 0) {
|
|
372
|
+
return {
|
|
373
|
+
...candidate,
|
|
374
|
+
displayVideoName: `${candidate.videoName} [${language}]`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (candidate.entry !== candidate.videoName) {
|
|
379
|
+
return { ...candidate, displayVideoName: candidate.entry };
|
|
380
|
+
}
|
|
381
|
+
return candidate;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
357
384
|
async function uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, credential, verbose, uploadAbort, progressReporter, progressIndex, recordId, expectedScreenshotCount) {
|
|
358
|
-
const { entry, videoName, data: rawData, preparedUploadAssets } = candidate;
|
|
385
|
+
const { entry, videoName, displayVideoName, data: rawData, preparedUploadAssets, } = candidate;
|
|
359
386
|
let projectId = null;
|
|
360
387
|
let videoId = null;
|
|
361
388
|
let plan = null;
|
|
@@ -376,8 +403,8 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
376
403
|
projectId: null,
|
|
377
404
|
videoId: null,
|
|
378
405
|
hadFailure: true,
|
|
379
|
-
videoName,
|
|
380
|
-
failureMessage: `Missing ${recordingFileName} for "${
|
|
406
|
+
videoName: displayVideoName,
|
|
407
|
+
failureMessage: `Missing ${recordingFileName} for "${displayVideoName}"`,
|
|
381
408
|
recordId,
|
|
382
409
|
};
|
|
383
410
|
}
|
|
@@ -392,8 +419,8 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
392
419
|
projectId: null,
|
|
393
420
|
videoId: null,
|
|
394
421
|
hadFailure: true,
|
|
395
|
-
videoName,
|
|
396
|
-
failureMessage: formatUnresolvedAssetMessage(
|
|
422
|
+
videoName: displayVideoName,
|
|
423
|
+
failureMessage: formatUnresolvedAssetMessage(displayVideoName, unresolved),
|
|
397
424
|
recordId,
|
|
398
425
|
};
|
|
399
426
|
}
|
|
@@ -437,7 +464,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
437
464
|
videoId: null,
|
|
438
465
|
hadFailure: true,
|
|
439
466
|
elevenLabsKeyMissing: true,
|
|
440
|
-
videoName,
|
|
467
|
+
videoName: displayVideoName,
|
|
441
468
|
recordId,
|
|
442
469
|
};
|
|
443
470
|
}
|
|
@@ -445,8 +472,8 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
445
472
|
projectId: null,
|
|
446
473
|
videoId: null,
|
|
447
474
|
hadFailure: true,
|
|
448
|
-
videoName,
|
|
449
|
-
failureMessage: formatUploadStartFailureMessage(
|
|
475
|
+
videoName: displayVideoName,
|
|
476
|
+
failureMessage: formatUploadStartFailureMessage(displayVideoName, startResponse.status, text, credential.value),
|
|
450
477
|
recordId,
|
|
451
478
|
};
|
|
452
479
|
}
|
|
@@ -461,7 +488,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
461
488
|
// the render for this video fails with the dependency error until fixed.
|
|
462
489
|
if (startBody.dependencyErrors && startBody.dependencyErrors.length > 0) {
|
|
463
490
|
for (const depError of startBody.dependencyErrors) {
|
|
464
|
-
logger.error(`Render dependency error in "${
|
|
491
|
+
logger.error(`Render dependency error in "${displayVideoName}": ${depError.detail}. This render will fail until it is fixed.`);
|
|
465
492
|
}
|
|
466
493
|
}
|
|
467
494
|
// A missing ElevenLabs key is a hard failure returned as an error response
|
|
@@ -508,8 +535,8 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
508
535
|
projectId,
|
|
509
536
|
videoId,
|
|
510
537
|
hadFailure: true,
|
|
511
|
-
videoName,
|
|
512
|
-
failureMessage: `Failed to upload recording for "${
|
|
538
|
+
videoName: displayVideoName,
|
|
539
|
+
failureMessage: `Failed to upload recording for "${displayVideoName}": ${recordingResponse.status} ${extractBackendError(text)}${hint401(recordingResponse.status, credential.value)}`,
|
|
513
540
|
recordId,
|
|
514
541
|
...(plan !== null && { plan }),
|
|
515
542
|
};
|
|
@@ -520,7 +547,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
520
547
|
projectId,
|
|
521
548
|
videoId,
|
|
522
549
|
hadFailure: false,
|
|
523
|
-
videoName,
|
|
550
|
+
videoName: displayVideoName,
|
|
524
551
|
recordId,
|
|
525
552
|
...(studio !== undefined && { studio }),
|
|
526
553
|
...(plan !== null && { plan }),
|
|
@@ -541,7 +568,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
541
568
|
projectId,
|
|
542
569
|
videoId,
|
|
543
570
|
hadFailure: true,
|
|
544
|
-
videoName,
|
|
571
|
+
videoName: displayVideoName,
|
|
545
572
|
failureMessage: err instanceof Error ? err.message : String(err),
|
|
546
573
|
recordId,
|
|
547
574
|
...(plan !== null && { plan }),
|
|
@@ -552,8 +579,8 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
552
579
|
projectId,
|
|
553
580
|
videoId,
|
|
554
581
|
hadFailure: true,
|
|
555
|
-
videoName,
|
|
556
|
-
failureMessage: `Network error uploading "${
|
|
582
|
+
videoName: displayVideoName,
|
|
583
|
+
failureMessage: `Network error uploading "${displayVideoName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
557
584
|
recordId,
|
|
558
585
|
...(plan !== null && { plan }),
|
|
559
586
|
};
|
|
@@ -1332,6 +1359,32 @@ export function formatFailedVideoMessage(videoName, message) {
|
|
|
1332
1359
|
}
|
|
1333
1360
|
return `${videoName}: ${message}`;
|
|
1334
1361
|
}
|
|
1362
|
+
function quoteFailedVideoName(videoName) {
|
|
1363
|
+
return `'${videoName.replaceAll("'", "\\'")}'`;
|
|
1364
|
+
}
|
|
1365
|
+
export function formatFailedVideoNamesSummary(videoNames) {
|
|
1366
|
+
if (videoNames.length === 0)
|
|
1367
|
+
return 'unknown';
|
|
1368
|
+
return videoNames
|
|
1369
|
+
.map((videoName) => quoteFailedVideoName(videoName))
|
|
1370
|
+
.join(', ');
|
|
1371
|
+
}
|
|
1372
|
+
export function collapseFailedVideoWarnings(failures) {
|
|
1373
|
+
const byMessage = new Map();
|
|
1374
|
+
for (const failure of failures) {
|
|
1375
|
+
const names = byMessage.get(failure.message);
|
|
1376
|
+
if (names)
|
|
1377
|
+
names.push(failure.videoName);
|
|
1378
|
+
else
|
|
1379
|
+
byMessage.set(failure.message, [failure.videoName]);
|
|
1380
|
+
}
|
|
1381
|
+
return [...byMessage.entries()].map(([message, videoNames]) => {
|
|
1382
|
+
if (videoNames.length === 1) {
|
|
1383
|
+
return formatFailedVideoMessage(videoNames[0] ?? 'unknown', message);
|
|
1384
|
+
}
|
|
1385
|
+
return message;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1335
1388
|
export function printUploadStartFailureMessage(videoName, status, responseText, secret) {
|
|
1336
1389
|
const message = formatUploadStartFailureMessage(videoName, status, responseText, secret);
|
|
1337
1390
|
if (responseText.trim().length > 0) {
|
|
@@ -1514,7 +1567,7 @@ async function uploadAssets(assets, apiUrl, credential, recordingId, signal, thr
|
|
|
1514
1567
|
}
|
|
1515
1568
|
}
|
|
1516
1569
|
}
|
|
1517
|
-
export async function uploadRecordings(screenciDir, projectName, apiUrl, credential, specificEntry, verbose = false) {
|
|
1570
|
+
export async function uploadRecordings(screenciDir, projectName, apiUrl, credential, specificEntry, verbose = false, allowedVideoNames) {
|
|
1518
1571
|
const uploadAbort = createUploadAbortController('upload');
|
|
1519
1572
|
const recordId = randomUUID();
|
|
1520
1573
|
let entries;
|
|
@@ -1527,6 +1580,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, credent
|
|
|
1527
1580
|
projectId: null,
|
|
1528
1581
|
recordId: null,
|
|
1529
1582
|
hadFailures: false,
|
|
1583
|
+
uploadedVideoNames: [],
|
|
1530
1584
|
failedVideoNames: [],
|
|
1531
1585
|
failedVideoMessages: [],
|
|
1532
1586
|
studioNotices: [],
|
|
@@ -1540,33 +1594,66 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, credent
|
|
|
1540
1594
|
}
|
|
1541
1595
|
let firstProjectId = null;
|
|
1542
1596
|
try {
|
|
1543
|
-
const candidates = (await Promise.all(entries.map(async (entry) => {
|
|
1597
|
+
const candidates = disambiguateUploadCandidateDisplayNames((await Promise.all(entries.map(async (entry) => {
|
|
1544
1598
|
uploadAbort.throwIfAborted();
|
|
1545
1599
|
return await loadUploadCandidate(screenciDir, entry, verbose);
|
|
1546
|
-
}))).filter((candidate) => candidate !== null);
|
|
1547
|
-
|
|
1600
|
+
}))).filter((candidate) => candidate !== null));
|
|
1601
|
+
const requestedVideoNames = allowedVideoNames !== undefined ? new Set(allowedVideoNames) : null;
|
|
1602
|
+
const filteredCandidates = requestedVideoNames === null
|
|
1603
|
+
? candidates
|
|
1604
|
+
: candidates.filter((candidate) => requestedVideoNames.has(candidate.videoName));
|
|
1605
|
+
if (filteredCandidates.length === 0) {
|
|
1606
|
+
const missingRequestedVideoNames = requestedVideoNames === null
|
|
1607
|
+
? []
|
|
1608
|
+
: [...requestedVideoNames].filter((videoName) => !candidates.some((candidate) => candidate.videoName === videoName));
|
|
1548
1609
|
return {
|
|
1549
1610
|
projectId: null,
|
|
1550
1611
|
recordId: null,
|
|
1551
|
-
hadFailures:
|
|
1552
|
-
|
|
1553
|
-
|
|
1612
|
+
hadFailures: missingRequestedVideoNames.length > 0,
|
|
1613
|
+
uploadedVideoNames: [],
|
|
1614
|
+
failedVideoNames: missingRequestedVideoNames,
|
|
1615
|
+
failedVideoMessages: missingRequestedVideoNames.map((videoName) => ({
|
|
1616
|
+
videoName,
|
|
1617
|
+
message: `No recorded output found for "${videoName}"`,
|
|
1618
|
+
})),
|
|
1554
1619
|
studioNotices: [],
|
|
1555
1620
|
elevenLabsKeyMissingVideos: [],
|
|
1556
1621
|
notices: [],
|
|
1557
1622
|
plan: null,
|
|
1558
1623
|
};
|
|
1559
1624
|
}
|
|
1560
|
-
|
|
1625
|
+
if (credential.header === ANON_TOKEN_HEADER &&
|
|
1626
|
+
filteredCandidates.length > ANON_MAX_VIDEOS_PER_RECORDING) {
|
|
1627
|
+
const failureMessage = `Anonymous trials are capped at ${ANON_MAX_VIDEOS_PER_RECORDING} videos/screenshots per recording. Split this into smaller runs or sign up to record more in one run.`;
|
|
1628
|
+
return {
|
|
1629
|
+
projectId: null,
|
|
1630
|
+
recordId: null,
|
|
1631
|
+
hadFailures: true,
|
|
1632
|
+
uploadedVideoNames: [],
|
|
1633
|
+
failedVideoNames: filteredCandidates.map((candidate) => candidate.displayVideoName),
|
|
1634
|
+
failedVideoMessages: filteredCandidates.map((candidate) => ({
|
|
1635
|
+
videoName: candidate.displayVideoName,
|
|
1636
|
+
message: failureMessage,
|
|
1637
|
+
})),
|
|
1638
|
+
studioNotices: [],
|
|
1639
|
+
elevenLabsKeyMissingVideos: [],
|
|
1640
|
+
notices: [],
|
|
1641
|
+
plan: null,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const progressReporter = createUploadProgressReporter(filteredCandidates.map((candidate) => candidate.displayVideoName), verbose);
|
|
1561
1645
|
// Screenshots from this run render together on one machine; the backend
|
|
1562
1646
|
// waits for all of them to land before dispatching the batch, so it needs
|
|
1563
1647
|
// to know how many to expect.
|
|
1564
|
-
const screenshotCount =
|
|
1565
|
-
const results = await Promise.all(
|
|
1648
|
+
const screenshotCount = filteredCandidates.filter((candidate) => candidate.data.output === 'screenshot').length;
|
|
1649
|
+
const results = await Promise.all(filteredCandidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, credential, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
|
|
1566
1650
|
firstProjectId =
|
|
1567
1651
|
results.find((result) => result.projectId !== null)?.projectId ?? null;
|
|
1568
1652
|
const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
|
|
1569
1653
|
const hadFailures = results.some((result) => result.hadFailure);
|
|
1654
|
+
const uploadedVideoNames = results
|
|
1655
|
+
.filter((result) => !result.hadFailure)
|
|
1656
|
+
.map((result) => result.videoName);
|
|
1570
1657
|
const failedVideoNames = results
|
|
1571
1658
|
.filter((result) => result.hadFailure)
|
|
1572
1659
|
.map((result) => result.videoName);
|
|
@@ -1588,6 +1675,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, credent
|
|
|
1588
1675
|
projectId: firstProjectId,
|
|
1589
1676
|
recordId,
|
|
1590
1677
|
hadFailures,
|
|
1678
|
+
uploadedVideoNames,
|
|
1591
1679
|
failedVideoNames,
|
|
1592
1680
|
failedVideoMessages,
|
|
1593
1681
|
studioNotices,
|
|
@@ -1616,6 +1704,35 @@ async function writeGitHubProjectOutput(projectUrl) {
|
|
|
1616
1704
|
return;
|
|
1617
1705
|
await appendFile(githubOutput, `screenci_project_url=${projectUrl}\n`);
|
|
1618
1706
|
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The builder titles each per-language Playwright test `${videoName} [${lang}]`
|
|
1709
|
+
* (src/builder.ts) so every language pass has a unique test title, while the
|
|
1710
|
+
* shared grouping key it writes to `metadata.videoName` carries NO language
|
|
1711
|
+
* suffix. `screenci record` discovers the videos to expect by their test titles,
|
|
1712
|
+
* but the uploader matches those against each recording's `metadata.videoName`.
|
|
1713
|
+
* Strip the trailing ` [<lang>]` so the requested name matches the recorded one;
|
|
1714
|
+
* otherwise a language-decorated title never matches and every upload reports
|
|
1715
|
+
* "No recorded output found". Only a language-code-shaped bracket is stripped, so
|
|
1716
|
+
* an unrelated trailing bracket in a video name is left intact.
|
|
1717
|
+
*/
|
|
1718
|
+
export function stripTestTitleLanguageSuffix(title) {
|
|
1719
|
+
// Language codes are lowercase (ISO 639: en, es, zh, ...), with an optional
|
|
1720
|
+
// region subtag (pt-BR). Requiring lowercase avoids stripping unrelated
|
|
1721
|
+
// capitalized brackets like ` [New]`.
|
|
1722
|
+
return title.replace(/ \[[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})?\]$/, '');
|
|
1723
|
+
}
|
|
1724
|
+
async function collectRequestedRecordVideoNames(configPath, additionalArgs, languages) {
|
|
1725
|
+
const envForDiscovery = {
|
|
1726
|
+
...process.env,
|
|
1727
|
+
SCREENCI_CONFIG_DIR: dirname(configPath),
|
|
1728
|
+
SCREENCI_RECORDING: 'true',
|
|
1729
|
+
...(languages ? { [SCREENCI_LANGUAGES_ENV]: languages } : {}),
|
|
1730
|
+
};
|
|
1731
|
+
const titles = await collectDiscoveredTestTitles(configPath, additionalArgs, envForDiscovery);
|
|
1732
|
+
// Requested names are matched against `metadata.videoName` (no language
|
|
1733
|
+
// suffix), so recover the videoName from each per-language test title.
|
|
1734
|
+
return [...new Set(titles.map(stripTestTitleLanguageSuffix))];
|
|
1735
|
+
}
|
|
1619
1736
|
async function loadScreenCIConfigAndEnv(configPath) {
|
|
1620
1737
|
const resolvedConfigPath = resolveScreenCIConfigPathOrExit(configPath);
|
|
1621
1738
|
let screenciConfig;
|
|
@@ -1938,6 +2055,134 @@ async function triggerRemoteRun(configPath, grep, languages) {
|
|
|
1938
2055
|
? `Triggered the remote recording workflow for "${screenciConfig.projectName}" (${filters.join(', ')}).`
|
|
1939
2056
|
: `Triggered the remote recording workflow for "${screenciConfig.projectName}".`);
|
|
1940
2057
|
}
|
|
2058
|
+
function getRecordRunLockPath(screenciDir) {
|
|
2059
|
+
return resolve(screenciDir, SCREENCI_RECORD_LOCK_FILE);
|
|
2060
|
+
}
|
|
2061
|
+
function defaultIsPidAlive(pid) {
|
|
2062
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
2063
|
+
return false;
|
|
2064
|
+
try {
|
|
2065
|
+
process.kill(pid, 0);
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
catch (error) {
|
|
2069
|
+
if (error instanceof Error && 'code' in error && error.code === 'ESRCH') {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function parseRecordRunLock(raw) {
|
|
2076
|
+
try {
|
|
2077
|
+
const parsed = JSON.parse(raw);
|
|
2078
|
+
return typeof parsed.pid === 'number' &&
|
|
2079
|
+
Number.isInteger(parsed.pid) &&
|
|
2080
|
+
parsed.pid > 0 &&
|
|
2081
|
+
typeof parsed.startedAt === 'string' &&
|
|
2082
|
+
parsed.startedAt.length > 0 &&
|
|
2083
|
+
typeof parsed.projectName === 'string' &&
|
|
2084
|
+
parsed.projectName.length > 0
|
|
2085
|
+
? {
|
|
2086
|
+
pid: parsed.pid,
|
|
2087
|
+
startedAt: parsed.startedAt,
|
|
2088
|
+
projectName: parsed.projectName,
|
|
2089
|
+
}
|
|
2090
|
+
: null;
|
|
2091
|
+
}
|
|
2092
|
+
catch {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function isRecordRunLockStale(lock, now, isPidAlive, maxAgeMs) {
|
|
2097
|
+
const startedAtMs = Date.parse(lock.startedAt);
|
|
2098
|
+
if (Number.isNaN(startedAtMs))
|
|
2099
|
+
return true;
|
|
2100
|
+
if (now.getTime() - startedAtMs > maxAgeMs)
|
|
2101
|
+
return true;
|
|
2102
|
+
return !isPidAlive(lock.pid);
|
|
2103
|
+
}
|
|
2104
|
+
function formatRecordRunLockError(lock) {
|
|
2105
|
+
return `Another 'screenci record' is in progress (pid ${lock.pid}, started ${lock.startedAt}, project "${lock.projectName}"). Wait for it or remove .screenci/.record.lock.`;
|
|
2106
|
+
}
|
|
2107
|
+
export async function acquireRecordRunLock(screenciDir, projectName, deps = {}) {
|
|
2108
|
+
const resolvedDeps = {
|
|
2109
|
+
fs: deps.fs ?? { mkdir, readFile, writeFile, rm },
|
|
2110
|
+
clock: deps.clock ?? (() => new Date()),
|
|
2111
|
+
isPidAlive: deps.isPidAlive ?? defaultIsPidAlive,
|
|
2112
|
+
pid: deps.pid ?? process.pid,
|
|
2113
|
+
addSignalListener: deps.addSignalListener ?? process.on.bind(process),
|
|
2114
|
+
removeSignalListener: deps.removeSignalListener ?? process.off.bind(process),
|
|
2115
|
+
removeLockSync: deps.removeLockSync ??
|
|
2116
|
+
((lockPath) => {
|
|
2117
|
+
rmSync(lockPath, { force: true });
|
|
2118
|
+
}),
|
|
2119
|
+
maxAgeMs: deps.maxAgeMs ?? SCREENCI_RECORD_LOCK_MAX_AGE_MS,
|
|
2120
|
+
};
|
|
2121
|
+
const lockPath = getRecordRunLockPath(screenciDir);
|
|
2122
|
+
const lock = {
|
|
2123
|
+
pid: resolvedDeps.pid,
|
|
2124
|
+
startedAt: resolvedDeps.clock().toISOString(),
|
|
2125
|
+
projectName,
|
|
2126
|
+
};
|
|
2127
|
+
await resolvedDeps.fs.mkdir(screenciDir, { recursive: true });
|
|
2128
|
+
for (;;) {
|
|
2129
|
+
try {
|
|
2130
|
+
await resolvedDeps.fs.writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, { flag: 'wx' });
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
2133
|
+
catch (error) {
|
|
2134
|
+
if (!(error instanceof Error) ||
|
|
2135
|
+
!('code' in error) ||
|
|
2136
|
+
error.code !== 'EEXIST') {
|
|
2137
|
+
throw error;
|
|
2138
|
+
}
|
|
2139
|
+
let existingLock = null;
|
|
2140
|
+
try {
|
|
2141
|
+
existingLock = parseRecordRunLock(await resolvedDeps.fs.readFile(lockPath, 'utf-8'));
|
|
2142
|
+
}
|
|
2143
|
+
catch {
|
|
2144
|
+
existingLock = null;
|
|
2145
|
+
}
|
|
2146
|
+
if (existingLock !== null &&
|
|
2147
|
+
!isRecordRunLockStale(existingLock, resolvedDeps.clock(), resolvedDeps.isPidAlive, resolvedDeps.maxAgeMs)) {
|
|
2148
|
+
throw new Error(formatRecordRunLockError(existingLock));
|
|
2149
|
+
}
|
|
2150
|
+
await resolvedDeps.fs.rm(lockPath, { force: true });
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
let released = false;
|
|
2154
|
+
const removeLockSync = () => {
|
|
2155
|
+
if (released)
|
|
2156
|
+
return;
|
|
2157
|
+
released = true;
|
|
2158
|
+
resolvedDeps.removeSignalListener('SIGINT', handleSigint);
|
|
2159
|
+
resolvedDeps.removeSignalListener('SIGTERM', handleSigterm);
|
|
2160
|
+
try {
|
|
2161
|
+
resolvedDeps.removeLockSync(lockPath);
|
|
2162
|
+
}
|
|
2163
|
+
catch {
|
|
2164
|
+
// best-effort during signal shutdown
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
const release = async () => {
|
|
2168
|
+
if (released)
|
|
2169
|
+
return;
|
|
2170
|
+
released = true;
|
|
2171
|
+
resolvedDeps.removeSignalListener('SIGINT', handleSigint);
|
|
2172
|
+
resolvedDeps.removeSignalListener('SIGTERM', handleSigterm);
|
|
2173
|
+
try {
|
|
2174
|
+
await resolvedDeps.fs.rm(lockPath, { force: true });
|
|
2175
|
+
}
|
|
2176
|
+
catch {
|
|
2177
|
+
// best-effort cleanup
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
const handleSigint = () => removeLockSync();
|
|
2181
|
+
const handleSigterm = () => removeLockSync();
|
|
2182
|
+
resolvedDeps.addSignalListener('SIGINT', handleSigint);
|
|
2183
|
+
resolvedDeps.addSignalListener('SIGTERM', handleSigterm);
|
|
2184
|
+
return { release };
|
|
2185
|
+
}
|
|
1941
2186
|
function getLastRecordFilePath(screenciDir) {
|
|
1942
2187
|
return resolve(screenciDir, SCREENCI_LAST_RECORD_FILE);
|
|
1943
2188
|
}
|
|
@@ -2043,14 +2288,18 @@ export async function ensureAnonRecordingAllowedOrExit(screenciDir, apiUrl, appU
|
|
|
2043
2288
|
// upload. Skip a `claimed` session: that user already accepted the
|
|
2044
2289
|
// versioned Terms when they signed up (the upload path self-upgrades).
|
|
2045
2290
|
if (status.status !== 'claimed') {
|
|
2046
|
-
logger.info(
|
|
2291
|
+
logger.info(formatAnonTermsNotice());
|
|
2047
2292
|
}
|
|
2048
2293
|
return;
|
|
2049
2294
|
}
|
|
2050
2295
|
const intro = gate.reason === 'expired'
|
|
2051
2296
|
? 'Your free ScreenCI trial has expired.'
|
|
2052
|
-
: "You've
|
|
2297
|
+
: "You've used all your free ScreenCI trial recordings.";
|
|
2298
|
+
const previousRecordUrl = gate.reason === 'used' ? await readAnonSessionRecordUrl(screenciDir) : null;
|
|
2053
2299
|
logger.error(`${intro}\n` +
|
|
2300
|
+
(previousRecordUrl
|
|
2301
|
+
? `Previous recording: ${pc.cyan(previousRecordUrl)}\n`
|
|
2302
|
+
: '') +
|
|
2054
2303
|
`Sign up to keep recording (no watermark, no limits): ${pc.cyan(appUrl)}\n` +
|
|
2055
2304
|
'After signing up, re-run this command in the same folder and it links automatically.');
|
|
2056
2305
|
process.exit(1);
|
|
@@ -2060,7 +2309,7 @@ export async function ensureAnonRecordingAllowedOrExit(screenciDir, apiUrl, appU
|
|
|
2060
2309
|
// re-sends the existing recordings without re-running Playwright). A non-null
|
|
2061
2310
|
// `playwrightFailure` means the preceding record run had failures, which tunes
|
|
2062
2311
|
// the messaging and the upload policy; `retry` always passes null.
|
|
2063
|
-
async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verbose) {
|
|
2312
|
+
async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verbose, requestedVideoNames) {
|
|
2064
2313
|
// After recording, upload results to API if configured. `run` already
|
|
2065
2314
|
// resolved the config (or exited), so this best-effort lookup only acts
|
|
2066
2315
|
// when a flat config is present in/under the current directory.
|
|
@@ -2098,6 +2347,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2098
2347
|
projectId: null,
|
|
2099
2348
|
recordId: null,
|
|
2100
2349
|
hadFailures: false,
|
|
2350
|
+
uploadedVideoNames: [],
|
|
2101
2351
|
failedVideoNames: [],
|
|
2102
2352
|
failedVideoMessages: [],
|
|
2103
2353
|
studioNotices: [],
|
|
@@ -2106,7 +2356,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2106
2356
|
plan: null,
|
|
2107
2357
|
};
|
|
2108
2358
|
try {
|
|
2109
|
-
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, credential, undefined, verbose);
|
|
2359
|
+
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, credential, undefined, verbose, requestedVideoNames);
|
|
2110
2360
|
}
|
|
2111
2361
|
catch (err) {
|
|
2112
2362
|
if (isUploadCancelledError(err)) {
|
|
@@ -2114,9 +2364,12 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2114
2364
|
}
|
|
2115
2365
|
throw err;
|
|
2116
2366
|
}
|
|
2117
|
-
const { projectId, recordId, hadFailures, failedVideoNames, failedVideoMessages, studioNotices, elevenLabsKeyMissingVideos, notices, plan, } = uploadResult;
|
|
2367
|
+
const { projectId, recordId, hadFailures, uploadedVideoNames, failedVideoNames, failedVideoMessages, studioNotices, elevenLabsKeyMissingVideos, notices, plan, } = uploadResult;
|
|
2368
|
+
const requestedUploadSucceeded = !hadFailures &&
|
|
2369
|
+
(requestedVideoNames === undefined ||
|
|
2370
|
+
requestedVideoNames.every((videoName) => uploadedVideoNames.includes(videoName)));
|
|
2118
2371
|
// Remember this run so `screenci info` can report exactly it.
|
|
2119
|
-
if (recordId !== null) {
|
|
2372
|
+
if (recordId !== null && requestedUploadSucceeded) {
|
|
2120
2373
|
await saveLastRecordId(screenciDir, recordId);
|
|
2121
2374
|
}
|
|
2122
2375
|
// Emit upload-failure warnings (stderr) before the results block.
|
|
@@ -2126,13 +2379,15 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2126
2379
|
// it from its URL. Reporting failures first keeps the URL directly
|
|
2127
2380
|
// under its message.
|
|
2128
2381
|
if (hadFailures) {
|
|
2129
|
-
for (const
|
|
2130
|
-
logger.warn(
|
|
2382
|
+
for (const warning of collapseFailedVideoWarnings(failedVideoMessages)) {
|
|
2383
|
+
logger.warn(warning);
|
|
2131
2384
|
}
|
|
2132
|
-
logger.warn(`Not all recordings succeeded to upload. Failed videos: ${failedVideoNames
|
|
2385
|
+
logger.warn(`Not all recordings succeeded to upload. Failed videos: ${formatFailedVideoNamesSummary(failedVideoNames)}. Some videos may be missing from the project.`);
|
|
2133
2386
|
}
|
|
2134
|
-
|
|
2387
|
+
let resultUrl = null;
|
|
2388
|
+
if (requestedUploadSucceeded && recordId !== null && projectId !== null) {
|
|
2135
2389
|
const recordUrl = `${appUrl}/record/${recordId}`;
|
|
2390
|
+
resultUrl = recordUrl;
|
|
2136
2391
|
await writeGitHubProjectOutput(recordUrl);
|
|
2137
2392
|
logger.info('');
|
|
2138
2393
|
logger.info(playwrightFailure !== null
|
|
@@ -2140,8 +2395,9 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2140
2395
|
: 'Recording finished, rendering in progress. Results available at:');
|
|
2141
2396
|
logger.info(pc.cyan(recordUrl));
|
|
2142
2397
|
}
|
|
2143
|
-
else if (projectId !== null) {
|
|
2398
|
+
else if (requestedUploadSucceeded && projectId !== null) {
|
|
2144
2399
|
const projectUrl = `${appUrl}/project/${projectId}`;
|
|
2400
|
+
resultUrl = projectUrl;
|
|
2145
2401
|
await writeGitHubProjectOutput(projectUrl);
|
|
2146
2402
|
logger.info('');
|
|
2147
2403
|
logger.info(playwrightFailure !== null
|
|
@@ -2149,8 +2405,25 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2149
2405
|
: 'Recording finished, rendering in progress. Results available at:');
|
|
2150
2406
|
logger.info(pc.cyan(projectUrl));
|
|
2151
2407
|
}
|
|
2152
|
-
if (usedAnonCredential &&
|
|
2153
|
-
|
|
2408
|
+
if (usedAnonCredential && resultUrl !== null) {
|
|
2409
|
+
try {
|
|
2410
|
+
await saveAnonSessionRecordUrl(screenciDir, credential.value, resultUrl);
|
|
2411
|
+
}
|
|
2412
|
+
catch (err) {
|
|
2413
|
+
logger.warn(`Failed to remember anonymous recording URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
2414
|
+
}
|
|
2415
|
+
// Report how many trial recordings remain after this run. Best-effort:
|
|
2416
|
+
// checkAnonSessionStatus never throws (it falls back on failure), so a
|
|
2417
|
+
// transient outage just shows the optimistic remaining count.
|
|
2418
|
+
const postStatus = await checkAnonSessionStatus(credential.value, {
|
|
2419
|
+
backendUrl: apiUrl,
|
|
2420
|
+
});
|
|
2421
|
+
if (postStatus.status === 'pending') {
|
|
2422
|
+
logger.info(formatAnonPostRecordNotice(postStatus.remaining));
|
|
2423
|
+
}
|
|
2424
|
+
else {
|
|
2425
|
+
logger.info(`Recorded without an account. Sign up to keep it.`);
|
|
2426
|
+
}
|
|
2154
2427
|
}
|
|
2155
2428
|
if (notices.length > 0) {
|
|
2156
2429
|
logger.info('');
|
|
@@ -2158,14 +2431,9 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2158
2431
|
logger.notice(notice);
|
|
2159
2432
|
}
|
|
2160
2433
|
}
|
|
2161
|
-
if (projectId !== null && plan
|
|
2434
|
+
if (projectId !== null && plan === 'starter') {
|
|
2162
2435
|
logger.info('');
|
|
2163
|
-
|
|
2164
|
-
logger.info('You are on the free tier, so this render includes a ScreenCI watermark. Upgrade to remove it and get more renders, more active videos, and expressive narration:');
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
logger.info('Upgrade for more renders, more active videos, and expressive narration:');
|
|
2168
|
-
}
|
|
2436
|
+
logger.info('Upgrade for more renders, more active videos, and expressive narration:');
|
|
2169
2437
|
logger.info(pc.cyan(`${appUrl}/select-plan`));
|
|
2170
2438
|
}
|
|
2171
2439
|
for (const notice of studioNotices) {
|
|
@@ -2229,35 +2497,49 @@ export async function main() {
|
|
|
2229
2497
|
await triggerRemoteRun(parsed.configPath, extractGrep(parsed.otherArgs), parsed.languages);
|
|
2230
2498
|
return;
|
|
2231
2499
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2500
|
+
validateArgs(parsed.otherArgs);
|
|
2501
|
+
const resolvedConfigPath = resolveScreenCIConfigPathOrExit(parsed.configPath);
|
|
2502
|
+
await loadEnvFileFromConfigSource(resolvedConfigPath, false);
|
|
2503
|
+
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
2504
|
+
const screenciDir = resolve(dirname(resolvedConfigPath), '.screenci');
|
|
2505
|
+
const requestedVideoNames = parsed.otherArgs.length > 0 || parsed.languages !== undefined
|
|
2506
|
+
? await collectRequestedRecordVideoNames(resolvedConfigPath, parsed.otherArgs, parsed.languages)
|
|
2507
|
+
: undefined;
|
|
2508
|
+
const recordRunLock = await acquireRecordRunLock(screenciDir, screenciConfig.projectName);
|
|
2509
|
+
try {
|
|
2510
|
+
let playwrightFailure = null;
|
|
2511
|
+
// UPLOAD_EXISTING re-sends the recordings already on disk under `.screenci`
|
|
2512
|
+
// without re-running Playwright (resend the last local run when only the
|
|
2513
|
+
// upload failed). We skip the recording run and fall straight through to
|
|
2514
|
+
// the upload below, treating the on-disk recordings as the complete set.
|
|
2515
|
+
const uploadExisting = isUploadExistingEnabled();
|
|
2516
|
+
if (!uploadExisting) {
|
|
2517
|
+
try {
|
|
2518
|
+
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.languages);
|
|
2247
2519
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2520
|
+
catch (error) {
|
|
2521
|
+
if (!(error instanceof Error))
|
|
2522
|
+
throw error;
|
|
2523
|
+
if (error.message.startsWith('Playwright exited with code ')) {
|
|
2524
|
+
playwrightFailure = new RecordFailureHintError(error);
|
|
2525
|
+
}
|
|
2526
|
+
else {
|
|
2527
|
+
throw new RecordFailureHintError(error);
|
|
2528
|
+
}
|
|
2250
2529
|
}
|
|
2251
2530
|
}
|
|
2531
|
+
else {
|
|
2532
|
+
logger.info('UPLOAD_EXISTING set: skipping Playwright recording and re-uploading existing .screenci recordings.');
|
|
2533
|
+
}
|
|
2534
|
+
if (process.env.SCREENCI_RECORDING === 'true')
|
|
2535
|
+
return;
|
|
2536
|
+
await uploadRecordedVideosForConfig(parsed.configPath, playwrightFailure, parsed.verbose, requestedVideoNames);
|
|
2537
|
+
if (playwrightFailure !== null) {
|
|
2538
|
+
throw playwrightFailure;
|
|
2539
|
+
}
|
|
2252
2540
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
}
|
|
2256
|
-
if (process.env.SCREENCI_RECORDING === 'true')
|
|
2257
|
-
return;
|
|
2258
|
-
await uploadRecordedVideosForConfig(parsed.configPath, playwrightFailure, parsed.verbose);
|
|
2259
|
-
if (playwrightFailure !== null) {
|
|
2260
|
-
throw playwrightFailure;
|
|
2541
|
+
finally {
|
|
2542
|
+
await recordRunLock.release();
|
|
2261
2543
|
}
|
|
2262
2544
|
});
|
|
2263
2545
|
program
|