screenci 0.0.9 → 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 +238 -55
- 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)) {
|
|
@@ -54,6 +128,8 @@ function parseArgs(args) {
|
|
|
54
128
|
}
|
|
55
129
|
let configPath;
|
|
56
130
|
let noContainer = false;
|
|
131
|
+
let imageTag;
|
|
132
|
+
let verbose = false;
|
|
57
133
|
const otherArgs = [];
|
|
58
134
|
for (let i = 1; i < args.length; i++) {
|
|
59
135
|
const arg = args[i];
|
|
@@ -71,11 +147,25 @@ function parseArgs(args) {
|
|
|
71
147
|
else if (arg === '--no-container') {
|
|
72
148
|
noContainer = true;
|
|
73
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
|
+
}
|
|
74
164
|
else if (arg !== undefined) {
|
|
75
165
|
otherArgs.push(arg);
|
|
76
166
|
}
|
|
77
167
|
}
|
|
78
|
-
return { command, configPath, noContainer, otherArgs };
|
|
168
|
+
return { command, configPath, noContainer, imageTag, verbose, otherArgs };
|
|
79
169
|
}
|
|
80
170
|
async function findLatestEntry(screenciDir) {
|
|
81
171
|
let entries;
|
|
@@ -187,11 +277,12 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
187
277
|
}
|
|
188
278
|
catch {
|
|
189
279
|
logger.warn('No .screenci directory found, skipping upload');
|
|
190
|
-
return;
|
|
280
|
+
return null;
|
|
191
281
|
}
|
|
192
282
|
if (specificEntry !== undefined) {
|
|
193
283
|
entries = entries.filter((e) => e === specificEntry);
|
|
194
284
|
}
|
|
285
|
+
let firstProjectId = null;
|
|
195
286
|
for (const entry of entries) {
|
|
196
287
|
const dataJsonPath = resolve(screenciDir, entry, 'data.json');
|
|
197
288
|
if (!existsSync(dataJsonPath))
|
|
@@ -206,7 +297,7 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
206
297
|
continue;
|
|
207
298
|
}
|
|
208
299
|
const videoName = data.metadata?.videoName ?? entry;
|
|
209
|
-
|
|
300
|
+
writeInline(`Uploading "${videoName}"...`);
|
|
210
301
|
try {
|
|
211
302
|
// Step 1: register upload and get recordingId
|
|
212
303
|
const startResponse = await fetch(`${apiUrl}/cli/upload/start`, {
|
|
@@ -219,10 +310,14 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
219
310
|
});
|
|
220
311
|
if (!startResponse.ok) {
|
|
221
312
|
const text = await startResponse.text();
|
|
313
|
+
process.stdout.write('\n');
|
|
222
314
|
logger.warn(`Failed to start upload for "${videoName}": ${startResponse.status} ${text}`);
|
|
223
315
|
continue;
|
|
224
316
|
}
|
|
225
|
-
const { recordingId } = (await startResponse.json());
|
|
317
|
+
const { recordingId, projectId } = (await startResponse.json());
|
|
318
|
+
if (firstProjectId === null) {
|
|
319
|
+
firstProjectId = projectId;
|
|
320
|
+
}
|
|
226
321
|
// Step 1b: upload asset files referenced in data.json
|
|
227
322
|
await uploadAssets(data, apiUrl, secret, recordingId, resolve(screenciDir, '..'));
|
|
228
323
|
// Step 2: stream the recording video file (if it exists)
|
|
@@ -243,16 +338,19 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
|
|
|
243
338
|
});
|
|
244
339
|
if (!recordingResponse.ok) {
|
|
245
340
|
const text = await recordingResponse.text();
|
|
341
|
+
process.stdout.write('\n');
|
|
246
342
|
logger.warn(`Failed to upload recording for "${videoName}": ${recordingResponse.status} ${text}`);
|
|
247
343
|
continue;
|
|
248
344
|
}
|
|
249
345
|
}
|
|
250
|
-
|
|
346
|
+
completeInline(`Uploading "${videoName}" ✓`);
|
|
251
347
|
}
|
|
252
348
|
catch (err) {
|
|
349
|
+
process.stdout.write('\n');
|
|
253
350
|
logger.warn(`Network error uploading "${videoName}":`, err);
|
|
254
351
|
}
|
|
255
352
|
}
|
|
353
|
+
return firstProjectId;
|
|
256
354
|
}
|
|
257
355
|
async function uploadLatest(configPath) {
|
|
258
356
|
const resolvedConfigPath = findScreenCIConfig(configPath);
|
|
@@ -281,11 +379,9 @@ async function uploadLatest(configPath) {
|
|
|
281
379
|
logger.warn(`Failed to load env file ${envFilePath}:`, err);
|
|
282
380
|
}
|
|
283
381
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
382
|
+
const apiUrl = process.env.DEV_PORT
|
|
383
|
+
? `http://localhost:${process.env.DEV_PORT}`
|
|
384
|
+
: 'https://api.screenci.com';
|
|
289
385
|
const secret = process.env.SCREENCI_SECRET;
|
|
290
386
|
if (!secret) {
|
|
291
387
|
logger.error('No secret configured. Set SCREENCI_SECRET in your .env file (get it from the API Key page in the dashboard).');
|
|
@@ -298,15 +394,24 @@ async function uploadLatest(configPath) {
|
|
|
298
394
|
logger.warn('No recordings found in .screenci directory');
|
|
299
395
|
return;
|
|
300
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';
|
|
301
402
|
logger.info(`Uploading latest recording: "${latestEntry}"`);
|
|
302
|
-
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
|
+
}
|
|
303
409
|
}
|
|
304
410
|
function generateConfig(projectName) {
|
|
305
411
|
return `import { defineConfig } from 'screenci'
|
|
306
412
|
|
|
307
413
|
export default defineConfig({
|
|
308
414
|
projectName: ${JSON.stringify(projectName)},
|
|
309
|
-
apiUrl: process.env.SCREENCI_URL ?? 'http://localhost:8787',
|
|
310
415
|
envFile: '.env',
|
|
311
416
|
videoDir: './videos',
|
|
312
417
|
forbidOnly: !!process.env.CI,
|
|
@@ -443,9 +548,9 @@ async function performBrowserLogin(appUrl) {
|
|
|
443
548
|
const port = server.address().port;
|
|
444
549
|
const callbackUrl = `http://localhost:${port}/callback`;
|
|
445
550
|
const loginUrl = `${appUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
446
|
-
logger.info('Opening browser for authentication...');
|
|
447
551
|
logger.info(`If the browser does not open automatically, visit:`);
|
|
448
|
-
logger.info(
|
|
552
|
+
logger.info(loginUrl);
|
|
553
|
+
logger.info('');
|
|
449
554
|
openBrowser(loginUrl);
|
|
450
555
|
});
|
|
451
556
|
const timeout = setTimeout(() => {
|
|
@@ -494,8 +599,9 @@ async function runInitAuth() {
|
|
|
494
599
|
(devPort ? `http://localhost:${devPort}` : 'https://app.screenci.com');
|
|
495
600
|
try {
|
|
496
601
|
const secret = await performBrowserLogin(appUrl);
|
|
497
|
-
|
|
498
|
-
|
|
602
|
+
const savePath = resolve(process.cwd(), '.env');
|
|
603
|
+
await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
|
|
604
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
|
|
499
605
|
}
|
|
500
606
|
catch (err) {
|
|
501
607
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -552,7 +658,7 @@ async function runInit(projectNameArg, localPackagePath) {
|
|
|
552
658
|
}
|
|
553
659
|
export async function main() {
|
|
554
660
|
const args = process.argv.slice(2);
|
|
555
|
-
const { command, configPath, noContainer, otherArgs } = parseArgs(args);
|
|
661
|
+
const { command, configPath, noContainer, imageTag, verbose, otherArgs } = parseArgs(args);
|
|
556
662
|
switch (command) {
|
|
557
663
|
case 'record': {
|
|
558
664
|
const useContainer = !noContainer && process.env.SCREENCI_IN_CONTAINER !== 'true';
|
|
@@ -589,7 +695,7 @@ export async function main() {
|
|
|
589
695
|
// Config import failed but SCREENCI_SECRET is already in env — continue
|
|
590
696
|
}
|
|
591
697
|
if (!process.env.SCREENCI_SECRET) {
|
|
592
|
-
logger.info('SCREENCI_SECRET
|
|
698
|
+
logger.info('No SCREENCI_SECRET in .env file, opening browser for authentication...');
|
|
593
699
|
const devPort = process.env.DEV_PORT;
|
|
594
700
|
const appUrl = process.env.SCREENCI_APP_URL ??
|
|
595
701
|
(devPort
|
|
@@ -599,12 +705,12 @@ export async function main() {
|
|
|
599
705
|
const savePath = envFilePath ?? resolve(dirname(resolvedConfigForSecret), '.env');
|
|
600
706
|
await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
|
|
601
707
|
process.env.SCREENCI_SECRET = secret;
|
|
602
|
-
logger.info(
|
|
708
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
|
|
603
709
|
}
|
|
604
710
|
}
|
|
605
711
|
}
|
|
606
712
|
if (useContainer) {
|
|
607
|
-
await runWithContainer(otherArgs, configPath);
|
|
713
|
+
await runWithContainer(otherArgs, configPath, imageTag, verbose);
|
|
608
714
|
}
|
|
609
715
|
else {
|
|
610
716
|
await run(command, otherArgs, configPath);
|
|
@@ -612,7 +718,7 @@ export async function main() {
|
|
|
612
718
|
// Upload only from the host, not from inside the container
|
|
613
719
|
if (process.env.SCREENCI_IN_CONTAINER === 'true')
|
|
614
720
|
break;
|
|
615
|
-
// After recording, upload results to
|
|
721
|
+
// After recording, upload results to API if configured
|
|
616
722
|
const resolvedConfigPath = findScreenCIConfig(configPath);
|
|
617
723
|
if (resolvedConfigPath) {
|
|
618
724
|
try {
|
|
@@ -627,11 +733,14 @@ export async function main() {
|
|
|
627
733
|
logger.warn(`Failed to load env file ${envFilePath}:`, err);
|
|
628
734
|
}
|
|
629
735
|
}
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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';
|
|
635
744
|
const secret = process.env.SCREENCI_SECRET;
|
|
636
745
|
if (!secret) {
|
|
637
746
|
logger.info('No secret configured, skipping upload. Set SCREENCI_SECRET in your .env file.');
|
|
@@ -639,7 +748,12 @@ export async function main() {
|
|
|
639
748
|
}
|
|
640
749
|
const configDir = dirname(resolvedConfigPath);
|
|
641
750
|
const screenciDir = resolve(configDir, '.screenci');
|
|
642
|
-
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
|
+
}
|
|
643
757
|
}
|
|
644
758
|
catch (err) {
|
|
645
759
|
logger.warn('Failed to load config for upload:', err);
|
|
@@ -746,7 +860,25 @@ export function detectContainerRuntime() {
|
|
|
746
860
|
logger.error(' docker: https://docs.docker.com/get-docker/');
|
|
747
861
|
process.exit(1);
|
|
748
862
|
}
|
|
749
|
-
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) {
|
|
750
882
|
const configPath = findScreenCIConfig(customConfigPath);
|
|
751
883
|
if (!configPath) {
|
|
752
884
|
const errorMsg = customConfigPath
|
|
@@ -768,43 +900,92 @@ async function runWithContainer(additionalArgs, customConfigPath) {
|
|
|
768
900
|
process.exit(1);
|
|
769
901
|
}
|
|
770
902
|
const containerRuntime = detectContainerRuntime();
|
|
903
|
+
const ghcrImage = 'ghcr.io/screenci/record:latest';
|
|
904
|
+
const dockerfileVersion = parseDockerfileVersion(dockerfilePath);
|
|
771
905
|
if (process.env['SCREENCI_LOCAL_IMAGE']) {
|
|
772
906
|
logger.info('SCREENCI_LOCAL_IMAGE set — skipping screenci image build');
|
|
773
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
|
+
}
|
|
774
919
|
else {
|
|
775
920
|
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
921
|
+
const screenciPackageRoot = resolve(cliDir, '..');
|
|
776
922
|
const screenciDockerfilePath = resolve(cliDir, 'Dockerfile');
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
}
|
|
799
974
|
clearDirectory(resolve(configDir, '.screenci'));
|
|
800
|
-
|
|
801
|
-
|
|
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, [
|
|
802
981
|
'run',
|
|
803
982
|
'--rm',
|
|
804
983
|
'-e',
|
|
805
984
|
'SCREENCI_IN_CONTAINER=true',
|
|
806
985
|
'-e',
|
|
807
986
|
'SCREENCI_RECORD=true',
|
|
987
|
+
'-e',
|
|
988
|
+
`SCREENCI_SECRET=${secret}`,
|
|
808
989
|
'-v',
|
|
809
990
|
`${configDir}/.screenci:/app/.screenci`,
|
|
810
991
|
'-v',
|
|
@@ -836,8 +1017,10 @@ async function run(command, additionalArgs, customConfigPath) {
|
|
|
836
1017
|
const isHeaded = additionalArgs.includes('--headed');
|
|
837
1018
|
const shouldUseUI = command === 'dev' && !isHeaded;
|
|
838
1019
|
const mode = command === 'dev' ? (isHeaded ? 'headed mode' : 'UI mode') : 'recorder';
|
|
839
|
-
|
|
840
|
-
|
|
1020
|
+
if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
|
|
1021
|
+
logger.info(`Running ScreenCI ${mode} with npx...`);
|
|
1022
|
+
logger.info(`Using config: ${configPath}`);
|
|
1023
|
+
}
|
|
841
1024
|
const playwrightArgs = [
|
|
842
1025
|
'playwright',
|
|
843
1026
|
'test',
|