screenci 0.0.8 → 0.0.10
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/Dockerfile +34 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +241 -56
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/captionHash.d.ts +12 -0
- package/dist/src/captionHash.d.ts.map +1 -0
- package/dist/src/captionHash.js +17 -0
- package/dist/src/captionHash.js.map +1 -0
- package/dist/src/events.d.ts +2 -2
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +30 -22
- package/dist/src/events.js.map +1 -1
- package/dist/src/recording.d.ts +4 -0
- package/dist/src/recording.d.ts.map +1 -0
- package/dist/src/recording.js +2 -0
- package/dist/src/recording.js.map +1 -0
- package/dist/src/types.d.ts +68 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +31 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +1 -4
- package/dist/src/video.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -1
package/dist/Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Recording runtime ───────────────────────────────────────────────────────────
|
|
2
|
+
FROM docker.io/library/node:25.2.1-slim
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
7
|
+
xvfb \
|
|
8
|
+
ffmpeg \
|
|
9
|
+
x11-utils \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# ── Dependency layer (cached until package.json changes) ──────────────────────
|
|
13
|
+
# Install screenci as a workspace package so npm creates the bin link.
|
|
14
|
+
COPY package.json ./screenci/
|
|
15
|
+
RUN printf '{"private":true,"workspaces":["screenci"]}' > package.json && npm install
|
|
16
|
+
|
|
17
|
+
# Playwright browser download: only re-runs when the playwright version changes.
|
|
18
|
+
RUN npx playwright install chromium --with-deps
|
|
19
|
+
|
|
20
|
+
# ── screenci build output ─────────────────────────────────────────────────────
|
|
21
|
+
# Copy pre-built dist directly from the screenci package directory.
|
|
22
|
+
COPY dist ./screenci/dist/
|
|
23
|
+
|
|
24
|
+
# Explicit bin wrapper — no npm bin-linking magic needed.
|
|
25
|
+
RUN printf '#!/bin/sh\nexec node /app/screenci/dist/cli.js "$@"\n' > /app/node_modules/.bin/screenci && \
|
|
26
|
+
chmod +x /app/node_modules/.bin/screenci
|
|
27
|
+
|
|
28
|
+
# Create .screenci directory for recordings
|
|
29
|
+
RUN mkdir -p .screenci
|
|
30
|
+
|
|
31
|
+
# Add node_modules/.bin to PATH
|
|
32
|
+
ENV PATH="/app/node_modules/.bin:${PATH}"
|
|
33
|
+
|
|
34
|
+
CMD ["echo", "Container ready"]
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AAgzBA,wBAAsB,IAAI,kBA2JzB;AAqED,wBAAgB,sBAAsB,IAAI,MAAM,CAc/C"}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { spawn, spawnSync } from 'child_process';
|
|
3
3
|
import { createReadStream } from 'fs';
|
|
4
|
-
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from 'fs';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, } from 'fs';
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
import { createServer } from 'http';
|
|
7
7
|
import { mkdir, readdir, readFile, stat, writeFile } from 'fs/promises';
|
|
@@ -9,6 +9,80 @@ import { dirname, relative as pathRelative, resolve } from 'path';
|
|
|
9
9
|
import { createInterface } from 'readline/promises';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { logger } from './src/logger.js';
|
|
12
|
+
function writeInline(msg) {
|
|
13
|
+
process.stdout.write(msg);
|
|
14
|
+
}
|
|
15
|
+
function completeInline(msg) {
|
|
16
|
+
process.stdout.write(`\r\x1b[K${msg}\n`);
|
|
17
|
+
}
|
|
18
|
+
function parseDockerfileVersion(dockerfilePath) {
|
|
19
|
+
let content;
|
|
20
|
+
try {
|
|
21
|
+
content = readFileSync(dockerfilePath, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 'unknown';
|
|
25
|
+
}
|
|
26
|
+
const fromLine = content
|
|
27
|
+
.split('\n')
|
|
28
|
+
.find((line) => line.trim().toUpperCase().startsWith('FROM'));
|
|
29
|
+
if (!fromLine)
|
|
30
|
+
return 'unknown';
|
|
31
|
+
const match = fromLine.match(/:([^\s@]+)/);
|
|
32
|
+
return match?.[1] ?? 'unknown';
|
|
33
|
+
}
|
|
34
|
+
function spawnSilent(cmd, args) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn(cmd, args, { stdio: 'pipe' });
|
|
37
|
+
child.on('close', (code) => {
|
|
38
|
+
if (code === 0) {
|
|
39
|
+
resolve();
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
reject(new Error(`${cmd} exited with code ${code}`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
child.on('error', reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const CONTAINER_LOG_FILTER = [
|
|
49
|
+
/^Running ScreenCI /,
|
|
50
|
+
/^Using config:/,
|
|
51
|
+
/^Starting Xvfb /,
|
|
52
|
+
/^Xvfb started /,
|
|
53
|
+
/^Recording video to:/,
|
|
54
|
+
/^Recording with /,
|
|
55
|
+
/^Stopping recording\.\.\./,
|
|
56
|
+
/^FFmpeg exited /,
|
|
57
|
+
/^Video saved to:/,
|
|
58
|
+
/^Events saved to:/,
|
|
59
|
+
];
|
|
60
|
+
function spawnContainerRecording(cmd, args) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const child = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
63
|
+
function forwardFiltered(chunk, out) {
|
|
64
|
+
const lines = chunk.toString().split('\n');
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (line === '')
|
|
67
|
+
continue;
|
|
68
|
+
if (!CONTAINER_LOG_FILTER.some((re) => re.test(line.trimStart()))) {
|
|
69
|
+
out.write(line + '\n');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
child.stdout?.on('data', (chunk) => forwardFiltered(chunk, process.stdout));
|
|
74
|
+
child.stderr?.on('data', (chunk) => forwardFiltered(chunk, process.stderr));
|
|
75
|
+
child.on('close', (code) => {
|
|
76
|
+
if (code === 0) {
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
reject(new Error(`${cmd} exited with code ${code}`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
child.on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
12
86
|
function clearDirectory(dir) {
|
|
13
87
|
mkdirSync(dir, { recursive: true });
|
|
14
88
|
for (const entry of readdirSync(dir)) {
|
|
@@ -34,7 +108,9 @@ function findRepoRoot(startDir) {
|
|
|
34
108
|
let current = startDir;
|
|
35
109
|
while (true) {
|
|
36
110
|
if (existsSync(resolve(current, '.git')) ||
|
|
37
|
-
existsSync(resolve(current, 'pnpm-workspace.yaml'))
|
|
111
|
+
existsSync(resolve(current, 'pnpm-workspace.yaml')) ||
|
|
112
|
+
existsSync(resolve(current, 'package-lock.json')) ||
|
|
113
|
+
existsSync(resolve(current, 'yarn.lock'))) {
|
|
38
114
|
return current;
|
|
39
115
|
}
|
|
40
116
|
const parent = resolve(current, '..');
|
|
@@ -52,6 +128,8 @@ function parseArgs(args) {
|
|
|
52
128
|
}
|
|
53
129
|
let configPath;
|
|
54
130
|
let noContainer = false;
|
|
131
|
+
let imageTag;
|
|
132
|
+
let verbose = false;
|
|
55
133
|
const otherArgs = [];
|
|
56
134
|
for (let i = 1; i < args.length; i++) {
|
|
57
135
|
const arg = args[i];
|
|
@@ -69,11 +147,25 @@ function parseArgs(args) {
|
|
|
69
147
|
else if (arg === '--no-container') {
|
|
70
148
|
noContainer = true;
|
|
71
149
|
}
|
|
150
|
+
else if (arg === '--verbose' || arg === '-v') {
|
|
151
|
+
verbose = true;
|
|
152
|
+
}
|
|
153
|
+
else if (arg === '--tag') {
|
|
154
|
+
const nextArg = args[i + 1];
|
|
155
|
+
if (nextArg !== undefined) {
|
|
156
|
+
imageTag = nextArg;
|
|
157
|
+
i++; // skip next arg
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
logger.error('Error: --tag requires a tag argument');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
72
164
|
else if (arg !== undefined) {
|
|
73
165
|
otherArgs.push(arg);
|
|
74
166
|
}
|
|
75
167
|
}
|
|
76
|
-
return { command, configPath, noContainer, otherArgs };
|
|
168
|
+
return { command, configPath, noContainer, imageTag, verbose, otherArgs };
|
|
77
169
|
}
|
|
78
170
|
async function findLatestEntry(screenciDir) {
|
|
79
171
|
let entries;
|
|
@@ -185,11 +277,12 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
185
277
|
}
|
|
186
278
|
catch {
|
|
187
279
|
logger.warn('No .screenci directory found, skipping upload');
|
|
188
|
-
return;
|
|
280
|
+
return null;
|
|
189
281
|
}
|
|
190
282
|
if (specificEntry !== undefined) {
|
|
191
283
|
entries = entries.filter((e) => e === specificEntry);
|
|
192
284
|
}
|
|
285
|
+
let firstProjectId = null;
|
|
193
286
|
for (const entry of entries) {
|
|
194
287
|
const dataJsonPath = resolve(screenciDir, entry, 'data.json');
|
|
195
288
|
if (!existsSync(dataJsonPath))
|
|
@@ -204,7 +297,7 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
204
297
|
continue;
|
|
205
298
|
}
|
|
206
299
|
const videoName = data.metadata?.videoName ?? entry;
|
|
207
|
-
|
|
300
|
+
writeInline(`Uploading "${videoName}"...`);
|
|
208
301
|
try {
|
|
209
302
|
// Step 1: register upload and get recordingId
|
|
210
303
|
const startResponse = await fetch(`${apiUrl}/cli/upload/start`, {
|
|
@@ -217,10 +310,14 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
217
310
|
});
|
|
218
311
|
if (!startResponse.ok) {
|
|
219
312
|
const text = await startResponse.text();
|
|
313
|
+
process.stdout.write('\n');
|
|
220
314
|
logger.warn(`Failed to start upload for "${videoName}": ${startResponse.status} ${text}`);
|
|
221
315
|
continue;
|
|
222
316
|
}
|
|
223
|
-
const { recordingId } = (await startResponse.json());
|
|
317
|
+
const { recordingId, projectId } = (await startResponse.json());
|
|
318
|
+
if (firstProjectId === null) {
|
|
319
|
+
firstProjectId = projectId;
|
|
320
|
+
}
|
|
224
321
|
// Step 1b: upload asset files referenced in data.json
|
|
225
322
|
await uploadAssets(data, apiUrl, secret, recordingId, resolve(screenciDir, '..'));
|
|
226
323
|
// Step 2: stream the recording video file (if it exists)
|
|
@@ -241,16 +338,19 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
241
338
|
});
|
|
242
339
|
if (!recordingResponse.ok) {
|
|
243
340
|
const text = await recordingResponse.text();
|
|
341
|
+
process.stdout.write('\n');
|
|
244
342
|
logger.warn(`Failed to upload recording for "${videoName}": ${recordingResponse.status} ${text}`);
|
|
245
343
|
continue;
|
|
246
344
|
}
|
|
247
345
|
}
|
|
248
|
-
|
|
346
|
+
completeInline(`Uploading "${videoName}" ✓`);
|
|
249
347
|
}
|
|
250
348
|
catch (err) {
|
|
349
|
+
process.stdout.write('\n');
|
|
251
350
|
logger.warn(`Network error uploading "${videoName}":`, err);
|
|
252
351
|
}
|
|
253
352
|
}
|
|
353
|
+
return firstProjectId;
|
|
254
354
|
}
|
|
255
355
|
async function uploadLatest(configPath) {
|
|
256
356
|
const resolvedConfigPath = findScreenCIConfig(configPath);
|
|
@@ -279,11 +379,9 @@ async function uploadLatest(configPath) {
|
|
|
279
379
|
logger.warn(`Failed to load env file ${envFilePath}:`, err);
|
|
280
380
|
}
|
|
281
381
|
}
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
382
|
+
const apiUrl = process.env.DEV_PORT
|
|
383
|
+
? `http://localhost:${process.env.DEV_PORT}`
|
|
384
|
+
: 'https://api.screenci.com';
|
|
287
385
|
const secret = process.env.SCREENCI_SECRET;
|
|
288
386
|
if (!secret) {
|
|
289
387
|
logger.error('No secret configured. Set SCREENCI_SECRET in your .env file (get it from the API Key page in the dashboard).');
|
|
@@ -296,15 +394,24 @@ async function uploadLatest(configPath) {
|
|
|
296
394
|
logger.warn('No recordings found in .screenci directory');
|
|
297
395
|
return;
|
|
298
396
|
}
|
|
397
|
+
const appUrl = process.env.SCREENCI_APP_URL
|
|
398
|
+
? process.env.SCREENCI_APP_URL
|
|
399
|
+
: process.env.DEV_PORT
|
|
400
|
+
? `http://localhost:${process.env.DEV_PORT}`
|
|
401
|
+
: 'https://app.screenci.com';
|
|
299
402
|
logger.info(`Uploading latest recording: "${latestEntry}"`);
|
|
300
|
-
await uploadRecordings(screenciDir, screenciConfig.projectName,
|
|
403
|
+
const projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret, latestEntry);
|
|
404
|
+
if (projectId !== null) {
|
|
405
|
+
logger.info('');
|
|
406
|
+
logger.info('Recording finished, results available at:');
|
|
407
|
+
logger.info(`${appUrl}/project/${projectId}`);
|
|
408
|
+
}
|
|
301
409
|
}
|
|
302
410
|
function generateConfig(projectName) {
|
|
303
411
|
return `import { defineConfig } from 'screenci'
|
|
304
412
|
|
|
305
413
|
export default defineConfig({
|
|
306
414
|
projectName: ${JSON.stringify(projectName)},
|
|
307
|
-
apiUrl: process.env.SCREENCI_URL ?? 'http://localhost:8787',
|
|
308
415
|
envFile: '.env',
|
|
309
416
|
videoDir: './videos',
|
|
310
417
|
forbidOnly: !!process.env.CI,
|
|
@@ -441,9 +548,9 @@ async function performBrowserLogin(appUrl) {
|
|
|
441
548
|
const port = server.address().port;
|
|
442
549
|
const callbackUrl = `http://localhost:${port}/callback`;
|
|
443
550
|
const loginUrl = `${appUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
444
|
-
logger.info('Opening browser for authentication...');
|
|
445
551
|
logger.info(`If the browser does not open automatically, visit:`);
|
|
446
|
-
logger.info(
|
|
552
|
+
logger.info(loginUrl);
|
|
553
|
+
logger.info('');
|
|
447
554
|
openBrowser(loginUrl);
|
|
448
555
|
});
|
|
449
556
|
const timeout = setTimeout(() => {
|
|
@@ -492,8 +599,9 @@ async function runInitAuth() {
|
|
|
492
599
|
(devPort ? `http://localhost:${devPort}` : 'https://app.screenci.com');
|
|
493
600
|
try {
|
|
494
601
|
const secret = await performBrowserLogin(appUrl);
|
|
495
|
-
|
|
496
|
-
|
|
602
|
+
const savePath = resolve(process.cwd(), '.env');
|
|
603
|
+
await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
|
|
604
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
|
|
497
605
|
}
|
|
498
606
|
catch (err) {
|
|
499
607
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -550,7 +658,7 @@ async function runInit(projectNameArg, localPackagePath) {
|
|
|
550
658
|
}
|
|
551
659
|
export async function main() {
|
|
552
660
|
const args = process.argv.slice(2);
|
|
553
|
-
const { command, configPath, noContainer, otherArgs } = parseArgs(args);
|
|
661
|
+
const { command, configPath, noContainer, imageTag, verbose, otherArgs } = parseArgs(args);
|
|
554
662
|
switch (command) {
|
|
555
663
|
case 'record': {
|
|
556
664
|
const useContainer = !noContainer && process.env.SCREENCI_IN_CONTAINER !== 'true';
|
|
@@ -587,7 +695,7 @@ export async function main() {
|
|
|
587
695
|
// Config import failed but SCREENCI_SECRET is already in env — continue
|
|
588
696
|
}
|
|
589
697
|
if (!process.env.SCREENCI_SECRET) {
|
|
590
|
-
logger.info('SCREENCI_SECRET
|
|
698
|
+
logger.info('No SCREENCI_SECRET in .env file, opening browser for authentication...');
|
|
591
699
|
const devPort = process.env.DEV_PORT;
|
|
592
700
|
const appUrl = process.env.SCREENCI_APP_URL ??
|
|
593
701
|
(devPort
|
|
@@ -597,12 +705,12 @@ export async function main() {
|
|
|
597
705
|
const savePath = envFilePath ?? resolve(dirname(resolvedConfigForSecret), '.env');
|
|
598
706
|
await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
|
|
599
707
|
process.env.SCREENCI_SECRET = secret;
|
|
600
|
-
logger.info(
|
|
708
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
|
|
601
709
|
}
|
|
602
710
|
}
|
|
603
711
|
}
|
|
604
712
|
if (useContainer) {
|
|
605
|
-
await runWithContainer(otherArgs, configPath);
|
|
713
|
+
await runWithContainer(otherArgs, configPath, imageTag, verbose);
|
|
606
714
|
}
|
|
607
715
|
else {
|
|
608
716
|
await run(command, otherArgs, configPath);
|
|
@@ -610,7 +718,7 @@ export async function main() {
|
|
|
610
718
|
// Upload only from the host, not from inside the container
|
|
611
719
|
if (process.env.SCREENCI_IN_CONTAINER === 'true')
|
|
612
720
|
break;
|
|
613
|
-
// After recording, upload results to
|
|
721
|
+
// After recording, upload results to API if configured
|
|
614
722
|
const resolvedConfigPath = findScreenCIConfig(configPath);
|
|
615
723
|
if (resolvedConfigPath) {
|
|
616
724
|
try {
|
|
@@ -625,11 +733,14 @@ export async function main() {
|
|
|
625
733
|
logger.warn(`Failed to load env file ${envFilePath}:`, err);
|
|
626
734
|
}
|
|
627
735
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
736
|
+
const apiUrl = process.env.DEV_PORT
|
|
737
|
+
? `http://localhost:${process.env.DEV_PORT}`
|
|
738
|
+
: 'https://api.screenci.com';
|
|
739
|
+
const appUrl = process.env.SCREENCI_APP_URL
|
|
740
|
+
? process.env.SCREENCI_APP_URL
|
|
741
|
+
: process.env.DEV_PORT
|
|
742
|
+
? `http://localhost:${process.env.DEV_PORT}`
|
|
743
|
+
: 'https://app.screenci.com';
|
|
633
744
|
const secret = process.env.SCREENCI_SECRET;
|
|
634
745
|
if (!secret) {
|
|
635
746
|
logger.info('No secret configured, skipping upload. Set SCREENCI_SECRET in your .env file.');
|
|
@@ -637,7 +748,12 @@ export async function main() {
|
|
|
637
748
|
}
|
|
638
749
|
const configDir = dirname(resolvedConfigPath);
|
|
639
750
|
const screenciDir = resolve(configDir, '.screenci');
|
|
640
|
-
await uploadRecordings(screenciDir, screenciConfig.projectName,
|
|
751
|
+
const projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret);
|
|
752
|
+
if (projectId !== null) {
|
|
753
|
+
logger.info('');
|
|
754
|
+
logger.info('Recording finished, results available at:');
|
|
755
|
+
logger.info(`${appUrl}/project/${projectId}`);
|
|
756
|
+
}
|
|
641
757
|
}
|
|
642
758
|
catch (err) {
|
|
643
759
|
logger.warn('Failed to load config for upload:', err);
|
|
@@ -744,7 +860,25 @@ export function detectContainerRuntime() {
|
|
|
744
860
|
logger.error(' docker: https://docs.docker.com/get-docker/');
|
|
745
861
|
process.exit(1);
|
|
746
862
|
}
|
|
747
|
-
async function
|
|
863
|
+
async function buildImage(cmd, args, label, verbose) {
|
|
864
|
+
if (verbose) {
|
|
865
|
+
await spawnInherited(cmd, args);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
writeInline(`${label}...`);
|
|
869
|
+
try {
|
|
870
|
+
await spawnSilent(cmd, args);
|
|
871
|
+
completeInline(`${label} ✓`);
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
process.stdout.write('\n');
|
|
875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
876
|
+
logger.error(msg);
|
|
877
|
+
logger.error('Run again with --verbose to see the full build output');
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function runWithContainer(additionalArgs, customConfigPath, imageTag, verbose = false) {
|
|
748
882
|
const configPath = findScreenCIConfig(customConfigPath);
|
|
749
883
|
if (!configPath) {
|
|
750
884
|
const errorMsg = customConfigPath
|
|
@@ -766,43 +900,92 @@ async function runWithContainer(additionalArgs, customConfigPath) {
|
|
|
766
900
|
process.exit(1);
|
|
767
901
|
}
|
|
768
902
|
const containerRuntime = detectContainerRuntime();
|
|
903
|
+
const ghcrImage = 'ghcr.io/screenci/record:latest';
|
|
904
|
+
const dockerfileVersion = parseDockerfileVersion(dockerfilePath);
|
|
769
905
|
if (process.env['SCREENCI_LOCAL_IMAGE']) {
|
|
770
906
|
logger.info('SCREENCI_LOCAL_IMAGE set — skipping screenci image build');
|
|
771
907
|
}
|
|
908
|
+
else if (imageTag !== undefined) {
|
|
909
|
+
const remoteImage = `ghcr.io/screenci/record:${imageTag}`;
|
|
910
|
+
const imageExists = spawnSync(containerRuntime, ['image', 'exists', remoteImage], {
|
|
911
|
+
stdio: 'ignore',
|
|
912
|
+
}).status === 0;
|
|
913
|
+
logger.info(`Using image tag ${imageTag} instead of the version ${dockerfileVersion} from Dockerfile`);
|
|
914
|
+
if (!imageExists) {
|
|
915
|
+
await buildImage(containerRuntime, ['pull', remoteImage], 'Pulling image', verbose);
|
|
916
|
+
}
|
|
917
|
+
await spawnSilent(containerRuntime, ['tag', remoteImage, ghcrImage]);
|
|
918
|
+
}
|
|
772
919
|
else {
|
|
773
920
|
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
921
|
+
const screenciPackageRoot = resolve(cliDir, '..');
|
|
774
922
|
const screenciDockerfilePath = resolve(cliDir, 'Dockerfile');
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
923
|
+
if (verbose) {
|
|
924
|
+
await spawnInherited(containerRuntime, [
|
|
925
|
+
'build',
|
|
926
|
+
'-f',
|
|
927
|
+
screenciDockerfilePath,
|
|
928
|
+
'-t',
|
|
929
|
+
ghcrImage,
|
|
930
|
+
screenciPackageRoot,
|
|
931
|
+
]);
|
|
932
|
+
await spawnInherited(containerRuntime, [
|
|
933
|
+
'build',
|
|
934
|
+
'-f',
|
|
935
|
+
dockerfilePath,
|
|
936
|
+
'-t',
|
|
937
|
+
'screenci',
|
|
938
|
+
configDir,
|
|
939
|
+
]);
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
writeInline('Building image...');
|
|
943
|
+
try {
|
|
944
|
+
await spawnSilent(containerRuntime, [
|
|
945
|
+
'build',
|
|
946
|
+
'-f',
|
|
947
|
+
screenciDockerfilePath,
|
|
948
|
+
'-t',
|
|
949
|
+
ghcrImage,
|
|
950
|
+
screenciPackageRoot,
|
|
951
|
+
]);
|
|
952
|
+
await spawnSilent(containerRuntime, [
|
|
953
|
+
'build',
|
|
954
|
+
'-f',
|
|
955
|
+
dockerfilePath,
|
|
956
|
+
'-t',
|
|
957
|
+
'screenci',
|
|
958
|
+
configDir,
|
|
959
|
+
]);
|
|
960
|
+
completeInline('Building image ✓');
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
process.stdout.write('\n');
|
|
964
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
965
|
+
logger.error(msg);
|
|
966
|
+
logger.error('Run again with --verbose to see the full build output');
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (imageTag !== undefined || process.env['SCREENCI_LOCAL_IMAGE']) {
|
|
972
|
+
await buildImage(containerRuntime, ['build', '-f', dockerfilePath, '-t', 'screenci', configDir], 'Building image', verbose);
|
|
973
|
+
}
|
|
797
974
|
clearDirectory(resolve(configDir, '.screenci'));
|
|
798
|
-
|
|
799
|
-
|
|
975
|
+
const secret = process.env['SCREENCI_SECRET'];
|
|
976
|
+
if (secret === undefined) {
|
|
977
|
+
logger.error('Error: SCREENCI_SECRET is not set');
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
await spawnContainerRecording(containerRuntime, [
|
|
800
981
|
'run',
|
|
801
982
|
'--rm',
|
|
802
983
|
'-e',
|
|
803
984
|
'SCREENCI_IN_CONTAINER=true',
|
|
804
985
|
'-e',
|
|
805
986
|
'SCREENCI_RECORD=true',
|
|
987
|
+
'-e',
|
|
988
|
+
`SCREENCI_SECRET=${secret}`,
|
|
806
989
|
'-v',
|
|
807
990
|
`${configDir}/.screenci:/app/.screenci`,
|
|
808
991
|
'-v',
|
|
@@ -834,8 +1017,10 @@ async function run(command, additionalArgs, customConfigPath) {
|
|
|
834
1017
|
const isHeaded = additionalArgs.includes('--headed');
|
|
835
1018
|
const shouldUseUI = command === 'dev' && !isHeaded;
|
|
836
1019
|
const mode = command === 'dev' ? (isHeaded ? 'headed mode' : 'UI mode') : 'recorder';
|
|
837
|
-
|
|
838
|
-
|
|
1020
|
+
if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
|
|
1021
|
+
logger.info(`Running ScreenCI ${mode} with npx...`);
|
|
1022
|
+
logger.info(`Using config: ${configPath}`);
|
|
1023
|
+
}
|
|
839
1024
|
const playwrightArgs = [
|
|
840
1025
|
'playwright',
|
|
841
1026
|
'test',
|