@invarn/cibuild 2.3.8 → 2.4.1
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.cjs +54 -37
- package/dist/src/cli.js +8 -0
- package/dist/src/commands/fidelity-trim.d.ts +13 -0
- package/dist/src/commands/fidelity-trim.d.ts.map +1 -0
- package/dist/src/commands/fidelity-trim.js +21 -0
- package/dist/src/commands/fidelity-trim.test.d.ts +8 -0
- package/dist/src/commands/fidelity-trim.test.d.ts.map +1 -0
- package/dist/src/commands/fidelity-trim.test.js +79 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts +52 -32
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -1
- package/dist/src/yaml/steps/render-post-processor.js +104 -205
- package/dist/src/yaml/steps/render-post-processor.test.d.ts +7 -10
- package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -1
- package/dist/src/yaml/steps/render-post-processor.test.js +80 -183
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +28 -0
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align-corpus.test.js +383 -0
- package/dist/src/yaml/steps/trim-align.d.ts +85 -0
- package/dist/src/yaml/steps/trim-align.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align.js +348 -0
- package/dist/src/yaml/steps/trim-align.test.d.ts +11 -0
- package/dist/src/yaml/steps/trim-align.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align.test.js +258 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +9 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +4 -3
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview.js +9 -0
- package/dist/src/yaml/steps/ui-fidelity-preview.test.js +13 -3
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +16 -5
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +190 -26
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +165 -10
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +20 -4
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +186 -10
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +180 -22
- package/package.json +3 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Swift toolchain (macOS with Xcode required, slow).
|
|
16
16
|
*/
|
|
17
17
|
import { describe, test, expect, afterAll } from '@jest/globals';
|
|
18
|
-
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
19
19
|
import { gzipSync } from 'node:zlib';
|
|
20
20
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
21
21
|
import { tmpdir } from 'node:os';
|
|
@@ -111,15 +111,24 @@ const FAKE_SWIFT_LINES = [
|
|
|
111
111
|
" printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out\"",
|
|
112
112
|
' printf \'fake-render:%s\' "$screen" >> "$out"',
|
|
113
113
|
' exit 0 ;;',
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
' *)',
|
|
115
|
+
' exit 0 ;;',
|
|
116
|
+
'esac',
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* PATH-shimmed fake `ci`, providing only the branch the shared trim stage
|
|
120
|
+
* uses: `ci fidelity-trim <in> <out> <aligned> <ref>`. Overwrites <out> with a
|
|
121
|
+
* PNG marked "trimmed:<in basename>" and writes an "aligned:<in basename>" PNG
|
|
122
|
+
* so tests can prove both outputs. Emits a fixed TRIMDIMS line (rendered
|
|
123
|
+
* 320x640, reference 160x320, source 400x800). FAKE_TRIM_FAIL exercises
|
|
124
|
+
* warn-and-continue.
|
|
125
|
+
*/
|
|
126
|
+
const FAKE_CI_LINES = [
|
|
127
|
+
'#!/bin/bash',
|
|
128
|
+
'case "$1" in',
|
|
129
|
+
' fidelity-trim)',
|
|
121
130
|
' if [ -n "$FAKE_TRIM_FAIL" ]; then echo "trim: simulated failure" >&2; exit 1; fi',
|
|
122
|
-
' in_png="$
|
|
131
|
+
' in_png="$2"; out_png="$3"; aligned_png="$4"',
|
|
123
132
|
" printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out_png\"",
|
|
124
133
|
' printf \'trimmed:%s\' "$(basename "$in_png")" >> "$out_png"',
|
|
125
134
|
' if [ -n "$aligned_png" ]; then',
|
|
@@ -128,8 +137,7 @@ const FAKE_SWIFT_LINES = [
|
|
|
128
137
|
' fi',
|
|
129
138
|
' echo "TRIMDIMS 320 640 160 320 400 800"',
|
|
130
139
|
' exit 0 ;;',
|
|
131
|
-
' *)',
|
|
132
|
-
' exit 0 ;;',
|
|
140
|
+
' *) exit 0 ;;',
|
|
133
141
|
'esac',
|
|
134
142
|
];
|
|
135
143
|
/**
|
|
@@ -186,6 +194,9 @@ function makeProject(params) {
|
|
|
186
194
|
const xcrunPath = join(binDir, 'xcrun');
|
|
187
195
|
writeFileSync(xcrunPath, FAKE_XCRUN_LINES.join('\n') + '\n');
|
|
188
196
|
chmodSync(xcrunPath, 0o755);
|
|
197
|
+
const ciPath = join(binDir, 'ci');
|
|
198
|
+
writeFileSync(ciPath, FAKE_CI_LINES.join('\n') + '\n');
|
|
199
|
+
chmodSync(ciPath, 0o755);
|
|
189
200
|
if (params !== undefined) {
|
|
190
201
|
writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), typeof params === 'string' ? params : JSON.stringify(params));
|
|
191
202
|
}
|
|
@@ -231,7 +242,9 @@ function makeTarGz(entries) {
|
|
|
231
242
|
const blocks = [];
|
|
232
243
|
for (const entry of entries) {
|
|
233
244
|
const isDir = entry.name.endsWith('/');
|
|
234
|
-
const content = Buffer.
|
|
245
|
+
const content = Buffer.isBuffer(entry.content)
|
|
246
|
+
? entry.content
|
|
247
|
+
: Buffer.from(entry.content ?? '', 'utf-8');
|
|
235
248
|
blocks.push(tarHeader(entry.name, isDir ? 0 : content.length, isDir ? '5' : '0'));
|
|
236
249
|
if (!isDir && content.length > 0) {
|
|
237
250
|
const padded = Buffer.alloc(Math.ceil(content.length / 512) * 512);
|
|
@@ -439,6 +452,157 @@ describe('package_source input', () => {
|
|
|
439
452
|
expect(inputsNames).toEqual(['swift', 'tar']);
|
|
440
453
|
});
|
|
441
454
|
});
|
|
455
|
+
// Clone-free package delivery (fidelity-linux 03): the dispatched YAML carries
|
|
456
|
+
// a signed package_url; the runtime downloads it to .ci/inputs/package.tar.gz
|
|
457
|
+
// and proceeds like inputs mode, resolving each screen's committed reference
|
|
458
|
+
// INSIDE the extracted package (repo-relative path minus the package_path
|
|
459
|
+
// prefix) — there is no checkout on a clone-free worker. Mirrors the Android
|
|
460
|
+
// step's url mode so both platform kinds accept the same dispatched inputs.
|
|
461
|
+
describe('package_source: url', () => {
|
|
462
|
+
/**
|
|
463
|
+
* Serve one tar.gz (or a 404) from a loopback HTTP server IN A CHILD
|
|
464
|
+
* PROCESS. runScript executes the generated script via spawnSync, which
|
|
465
|
+
* blocks this process's event loop — an in-process server would deadlock
|
|
466
|
+
* against the script's curl.
|
|
467
|
+
*/
|
|
468
|
+
const SERVE_PACKAGE_SRC = [
|
|
469
|
+
"var http = require('http');",
|
|
470
|
+
"var fs = require('fs');",
|
|
471
|
+
'var filePath = process.argv[1];',
|
|
472
|
+
'var server = http.createServer(function (req, res) {',
|
|
473
|
+
" if (filePath === '-') { res.statusCode = 404; res.end('gone'); return; }",
|
|
474
|
+
" res.setHeader('content-type', 'application/gzip');",
|
|
475
|
+
' res.end(fs.readFileSync(filePath));',
|
|
476
|
+
'});',
|
|
477
|
+
"server.listen(0, '127.0.0.1', function () {",
|
|
478
|
+
" console.log('PORT ' + server.address().port);",
|
|
479
|
+
'});',
|
|
480
|
+
].join('\n');
|
|
481
|
+
async function servePackage(archive) {
|
|
482
|
+
let filePath = '-';
|
|
483
|
+
if (archive !== null) {
|
|
484
|
+
const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-url-serve-'));
|
|
485
|
+
tempDirs.push(dir);
|
|
486
|
+
filePath = join(dir, 'package.tar.gz');
|
|
487
|
+
writeFileSync(filePath, archive);
|
|
488
|
+
}
|
|
489
|
+
const child = spawn('node', ['-e', SERVE_PACKAGE_SRC, filePath], {
|
|
490
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
491
|
+
});
|
|
492
|
+
const port = await new Promise((resolvePort, rejectPort) => {
|
|
493
|
+
let buffered = '';
|
|
494
|
+
const timer = setTimeout(() => rejectPort(new Error('package server did not start')), 10_000);
|
|
495
|
+
child.stdout.on('data', (chunk) => {
|
|
496
|
+
buffered += chunk.toString();
|
|
497
|
+
const match = /PORT (\d+)/.exec(buffered);
|
|
498
|
+
if (match) {
|
|
499
|
+
clearTimeout(timer);
|
|
500
|
+
resolvePort(Number(match[1]));
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
child.on('exit', () => rejectPort(new Error('package server exited early')));
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
url: `http://127.0.0.1:${port}/api/fidelity/packages/blob1?exp=1&sig=ab`,
|
|
507
|
+
close: () => new Promise((resolveClose) => {
|
|
508
|
+
child.once('exit', () => resolveClose(undefined));
|
|
509
|
+
child.kill('SIGKILL');
|
|
510
|
+
}),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** Archive shaped like the dashboard's assembly: package-dir contents at the
|
|
514
|
+
* archive root, committed references under references/. */
|
|
515
|
+
function packagedProjectWithReference() {
|
|
516
|
+
return makeTarGz([
|
|
517
|
+
...shippedPackageEntries(''),
|
|
518
|
+
{
|
|
519
|
+
name: 'references/home.png',
|
|
520
|
+
content: Buffer.concat([PNG_MAGIC, Buffer.from('ref:url-home')]),
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
}
|
|
524
|
+
test('package_source url requires package_url and package_path', async () => {
|
|
525
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
526
|
+
await expect(executor.execute({ package_source: 'url', package_path: 'ios-pkg' }, {}, testConfig)).rejects.toThrow(/package_url/);
|
|
527
|
+
await expect(executor.execute({ package_source: 'url', package_url: 'https://x.test/p' }, {}, testConfig)).rejects.toThrow(/package_path/);
|
|
528
|
+
});
|
|
529
|
+
test('rejects reference_source alongside package_source url', async () => {
|
|
530
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
531
|
+
await expect(executor.execute({
|
|
532
|
+
package_source: 'url',
|
|
533
|
+
package_url: 'https://x.test/p',
|
|
534
|
+
package_path: 'ios-pkg',
|
|
535
|
+
reference_source: 'repo',
|
|
536
|
+
}, {}, testConfig)).rejects.toThrow(/reference_source/);
|
|
537
|
+
});
|
|
538
|
+
test('url mode requires swift, tar, and curl', () => {
|
|
539
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
540
|
+
const names = executor
|
|
541
|
+
.getValidationRequirements({ package_source: 'url' }, {}, testConfig)
|
|
542
|
+
.map((requirement) => requirement.name);
|
|
543
|
+
expect(names).toEqual(['swift', 'tar', 'curl']);
|
|
544
|
+
});
|
|
545
|
+
test('downloads the package, renders, and resolves references inside it', async () => {
|
|
546
|
+
const project = makeProject({ screens: { HomeView: 'ios-pkg/references/home.png' } });
|
|
547
|
+
const served = await servePackage(packagedProjectWithReference());
|
|
548
|
+
try {
|
|
549
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
550
|
+
const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
|
|
551
|
+
const run = runScript(project, step.script);
|
|
552
|
+
expect(run.status).toBe(0);
|
|
553
|
+
const doc = readResult(project);
|
|
554
|
+
expect(doc.package_source).toBe('url');
|
|
555
|
+
const screen = doc.screens.find((s) => s.screen === 'HomeView');
|
|
556
|
+
expect(screen?.status).toBe('rendered');
|
|
557
|
+
expect(screen?.reference_image_path).toBe('ui-fidelity/references/HomeView.png');
|
|
558
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
await served.close();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
test('a failed download is one structured build error, not per-screen noise', async () => {
|
|
565
|
+
const project = makeProject({ screens: { HomeView: 'ios-pkg/references/home.png' } });
|
|
566
|
+
const served = await servePackage(null); // 404
|
|
567
|
+
try {
|
|
568
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
569
|
+
const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
|
|
570
|
+
const run = runScript(project, step.script);
|
|
571
|
+
expect(run.status).not.toBe(0);
|
|
572
|
+
const doc = readResult(project);
|
|
573
|
+
expect(doc.error?.code).toBe('PACKAGE_DOWNLOAD_FAILED');
|
|
574
|
+
const screen = doc.screens.find((s) => s.screen === 'HomeView');
|
|
575
|
+
expect(screen?.error).toBeNull();
|
|
576
|
+
expectNoAbsolutePathLeaks(project, doc);
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
await served.close();
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
test('a reference outside the package directory fails that screen only', async () => {
|
|
583
|
+
const project = makeProject({
|
|
584
|
+
screens: {
|
|
585
|
+
HomeView: 'ios-pkg/references/home.png',
|
|
586
|
+
Stray: 'design/elsewhere/stray.png',
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
const served = await servePackage(packagedProjectWithReference());
|
|
590
|
+
try {
|
|
591
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
592
|
+
const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
|
|
593
|
+
const run = runScript(project, step.script, { FAKE_SWIFT_RUN_SILENT: 'Stray' });
|
|
594
|
+
expect(run.status).not.toBe(0);
|
|
595
|
+
const doc = readResult(project);
|
|
596
|
+
expect(doc.screens.find((s) => s.screen === 'HomeView')?.status).toBe('rendered');
|
|
597
|
+
const stray = doc.screens.find((s) => s.screen === 'Stray');
|
|
598
|
+
expect(stray?.status).toBe('render_failed');
|
|
599
|
+
expect(stray?.error?.code).toBe('REFERENCE_MISSING');
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
await served.close();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
});
|
|
442
606
|
describe('render size and scale parsing', () => {
|
|
443
607
|
test('default render size is 393x852 device points', () => {
|
|
444
608
|
expect(DEFAULT_RENDER_SIZE).toBe('393x852');
|
|
@@ -1016,20 +1180,14 @@ describe('render output trimming (package_source variants)', () => {
|
|
|
1016
1180
|
function readArtifact(project, relative) {
|
|
1017
1181
|
return readFileSync(artifact(project, relative), 'utf-8');
|
|
1018
1182
|
}
|
|
1019
|
-
test('trims the rendered PNG in place via the
|
|
1183
|
+
test('trims the rendered PNG in place via the ci fidelity-trim helper', async () => {
|
|
1020
1184
|
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1021
1185
|
writeReference(project, 'home.png');
|
|
1022
1186
|
const script = await buildScript(project, { package_source: 'repo' });
|
|
1023
|
-
const
|
|
1024
|
-
const run = runScript(project, script, { FAKE_SWIFT_LOG: logPath });
|
|
1187
|
+
const run = runScript(project, script);
|
|
1025
1188
|
expect(run.status).toBe(0);
|
|
1026
|
-
// The
|
|
1027
|
-
// (in
|
|
1028
|
-
const invocations = readFileSync(logPath, 'utf-8').trim().split('\n');
|
|
1029
|
-
const trimCall = invocations.find((c) => c.includes('Trim.swift'));
|
|
1030
|
-
expect(trimCall).toBeTruthy();
|
|
1031
|
-
expect(trimCall).toContain('ui-fidelity/rendered/HomeView.png');
|
|
1032
|
-
// The rendered file now holds the trim helper's output, not the raw render.
|
|
1189
|
+
// The rendered file now holds the trim helper's output, not the raw render
|
|
1190
|
+
// (the fake ci overwrites in place, proving the in/out in-place contract).
|
|
1033
1191
|
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
1034
1192
|
expect(readArtifact(project, 'ui-fidelity/rendered/HomeView.png')).toContain('trimmed:HomeView.png');
|
|
1035
1193
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invarn/cibuild",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "CI Build CLI — local pipeline orchestration and validation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.cjs",
|
|
@@ -55,12 +55,14 @@
|
|
|
55
55
|
"homepage": "https://github.com/invarnhq/cibuild",
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
|
+
"pngjs": "7.0.0",
|
|
58
59
|
"prompts": "^2.4.2"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@types/jest": "^29.5.14",
|
|
62
63
|
"@types/js-yaml": "^4.0.9",
|
|
63
64
|
"@types/node": "^22.10.1",
|
|
65
|
+
"@types/pngjs": "6.0.5",
|
|
64
66
|
"@types/prompts": "^2.4.9",
|
|
65
67
|
"@yao-pkg/pkg": "^6.14.1",
|
|
66
68
|
"esbuild": "^0.27.3",
|