@invarn/cibuild 2.2.6 → 2.2.8
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 +133 -9
- package/dist/src/yaml/fidelity-input-digest.d.ts +37 -0
- package/dist/src/yaml/fidelity-input-digest.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-input-digest.js +208 -0
- package/dist/src/yaml/fidelity-input-digest.test.d.ts +2 -0
- package/dist/src/yaml/fidelity-input-digest.test.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-input-digest.test.js +167 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +29 -2
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +38 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +195 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +207 -1
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +21 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +89 -3
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +71 -0
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
import { BaseStepExecutor } from './base.js';
|
|
52
52
|
import { RENDER_SCRIPT_TRIM_STAGE } from './render-post-processor.js';
|
|
53
|
+
import { RENDER_SCRIPT_SOURCE_DIGEST_STAGE } from '../fidelity-input-digest.js';
|
|
53
54
|
/**
|
|
54
55
|
* Default render size in device points. 393x852 is the iPhone-class portrait
|
|
55
56
|
* canvas shared by the iPhone 14 Pro / 15 / 16 — the most common modern
|
|
@@ -63,6 +64,10 @@ export const DEFAULT_SCALE = 2;
|
|
|
63
64
|
export const PACKAGE_SOURCES = ['repo', 'inputs'];
|
|
64
65
|
/** Default package source: the package lives in the repo checkout. */
|
|
65
66
|
export const DEFAULT_PACKAGE_SOURCE = 'repo';
|
|
67
|
+
/** Valid values for the reference_source input. */
|
|
68
|
+
export const REFERENCE_SOURCES = ['repo', 'inputs'];
|
|
69
|
+
/** Default reference source: references arrive as run inputs (v1 behaviour). */
|
|
70
|
+
export const DEFAULT_REFERENCE_SOURCE = 'inputs';
|
|
66
71
|
/** Basename of the shipped package archive inside the run-inputs directory. */
|
|
67
72
|
export const PACKAGE_ARCHIVE_BASENAME = 'package.tar.gz';
|
|
68
73
|
/**
|
|
@@ -335,6 +340,13 @@ function runSwift(args) {
|
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
function renderWithHarness(renderable, packagePath) {
|
|
343
|
+
// Snapshot the user package's input files BEFORE any Swift build, so build
|
|
344
|
+
// products (.build, DerivedData) never perturb the content key. A package is
|
|
345
|
+
// safely cacheable only when it is hermetic — its own files are its whole
|
|
346
|
+
// dependency closure; a non-hermetic one emits no digest and so is never
|
|
347
|
+
// skipped by a consumer.
|
|
348
|
+
var packageFiles = __fidCollectPackageFiles(packagePath);
|
|
349
|
+
var packageHermetic = __fidIsHermetic(packageFiles);
|
|
338
350
|
var harnessDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-fidelity-harness-'));
|
|
339
351
|
registerPathReplacement(harnessDir, '<harness>');
|
|
340
352
|
log('harness package: ' + harnessDir);
|
|
@@ -416,6 +428,20 @@ function renderWithHarness(renderable, packagePath) {
|
|
|
416
428
|
entry.rendered_image_path = relative;
|
|
417
429
|
entry.status = 'rendered';
|
|
418
430
|
entry.error = null;
|
|
431
|
+
// Content key over the package source + this screen's reference: lets a
|
|
432
|
+
// consumer skip a re-render whose inputs are unchanged. Emitted only for a
|
|
433
|
+
// hermetic package; otherwise omitted so no stale verdict can be replayed.
|
|
434
|
+
if (packageHermetic) {
|
|
435
|
+
var referenceBytes = null;
|
|
436
|
+
try {
|
|
437
|
+
if (entry.reference_image_path) {
|
|
438
|
+
referenceBytes = fs.readFileSync(path.resolve(artifactsDir, entry.reference_image_path));
|
|
439
|
+
}
|
|
440
|
+
} catch (digestReferenceError) {
|
|
441
|
+
referenceBytes = null;
|
|
442
|
+
}
|
|
443
|
+
entry.source_digest = __fidSourceDigest(packageFiles, referenceBytes);
|
|
444
|
+
}
|
|
419
445
|
log('rendered ' + entry.screen + ' -> ' + relative);
|
|
420
446
|
});
|
|
421
447
|
} finally {
|
|
@@ -1112,6 +1138,41 @@ function buildPackageSourceMain() {
|
|
|
1112
1138
|
return main;
|
|
1113
1139
|
}
|
|
1114
1140
|
const RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE = buildPackageSourceMain();
|
|
1141
|
+
/**
|
|
1142
|
+
* Patches the reference-sourcing block to read each screen's reference from a
|
|
1143
|
+
* repo-relative path (resolved against the checkout) instead of a plain
|
|
1144
|
+
* basename under `.ci/inputs/`. Applied on top of EITHER base main (v1 or the
|
|
1145
|
+
* package_source variant) — the package patches never touch this block, so the
|
|
1146
|
+
* anchor is present and unique in both. The downstream stat/copy logic is
|
|
1147
|
+
* unchanged; only how `source` is computed differs.
|
|
1148
|
+
*/
|
|
1149
|
+
function buildReferenceRepoMain(base) {
|
|
1150
|
+
return replaceOnce(base, 'var basename = params.screens[entry.screen];\n' +
|
|
1151
|
+
' if (\n' +
|
|
1152
|
+
" typeof basename !== 'string' || basename === '' ||\n" +
|
|
1153
|
+
" basename === '.' || basename === '..' ||\n" +
|
|
1154
|
+
' basename !== path.basename(basename)\n' +
|
|
1155
|
+
' ) {\n' +
|
|
1156
|
+
' setError(\n' +
|
|
1157
|
+
' entry,\n' +
|
|
1158
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1159
|
+
" 'reference for ' + entry.screen + ' must be a plain file basename inside ' +\n" +
|
|
1160
|
+
" inputsDir + ', got: ' + JSON.stringify(basename)\n" +
|
|
1161
|
+
' );\n' +
|
|
1162
|
+
' return;\n' +
|
|
1163
|
+
' }\n' +
|
|
1164
|
+
' var source = path.join(inputsDir, basename);', 'var refPath = params.screens[entry.screen];\n' +
|
|
1165
|
+
" if (typeof refPath !== 'string' || refPath === '') {\n" +
|
|
1166
|
+
' setError(\n' +
|
|
1167
|
+
' entry,\n' +
|
|
1168
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1169
|
+
" 'reference for ' + entry.screen + ' must be a non-empty repo-relative path, got: ' +\n" +
|
|
1170
|
+
' JSON.stringify(refPath)\n' +
|
|
1171
|
+
' );\n' +
|
|
1172
|
+
' return;\n' +
|
|
1173
|
+
' }\n' +
|
|
1174
|
+
' var source = path.resolve(refPath);');
|
|
1175
|
+
}
|
|
1115
1176
|
/**
|
|
1116
1177
|
* Evaluates the embedded generator source and returns the real functions, so
|
|
1117
1178
|
* unit tests exercise exactly the code that ships inside the runtime script.
|
|
@@ -1133,14 +1194,21 @@ export function getRenderScriptInternals() {
|
|
|
1133
1194
|
* unpatched v1 runtime is used).
|
|
1134
1195
|
*/
|
|
1135
1196
|
export function generateRenderScript(config) {
|
|
1197
|
+
let main = config.packageSource === undefined
|
|
1198
|
+
? RENDER_SCRIPT_MAIN
|
|
1199
|
+
: RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
|
|
1200
|
+
// Reference-from-repo is an independent dimension layered on top of whichever
|
|
1201
|
+
// package base applies. Absent → byte-identical reference block (v1).
|
|
1202
|
+
if (config.referenceSource === 'repo') {
|
|
1203
|
+
main = buildReferenceRepoMain(main);
|
|
1204
|
+
}
|
|
1136
1205
|
return [
|
|
1137
1206
|
"'use strict';",
|
|
1138
1207
|
'// ui-fidelity-render runtime script (generated by cibuild at YAML conversion time)',
|
|
1139
1208
|
'var CONFIG = ' + JSON.stringify(config) + ';',
|
|
1140
1209
|
RENDER_SCRIPT_GENERATORS,
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
: RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE,
|
|
1210
|
+
RENDER_SCRIPT_SOURCE_DIGEST_STAGE,
|
|
1211
|
+
main,
|
|
1144
1212
|
].join('\n');
|
|
1145
1213
|
}
|
|
1146
1214
|
/**
|
|
@@ -1196,12 +1264,30 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
|
|
|
1196
1264
|
throw new Error(`Invalid target '${target}' for step '${stepName}': ` +
|
|
1197
1265
|
'must be a Swift module identifier (letters, digits, underscores)');
|
|
1198
1266
|
}
|
|
1267
|
+
// reference_source mirrors package_source: validate when provided, but only
|
|
1268
|
+
// record (and patch the script for) the non-default "repo" so omitting it —
|
|
1269
|
+
// or setting the default "inputs" — keeps the v1 reference block intact.
|
|
1270
|
+
const rawReferenceSource = this.getInput(inputs, 'reference_source', undefined);
|
|
1271
|
+
let referenceSource;
|
|
1272
|
+
if (rawReferenceSource !== undefined) {
|
|
1273
|
+
const value = String(rawReferenceSource);
|
|
1274
|
+
if (!REFERENCE_SOURCES.includes(value)) {
|
|
1275
|
+
throw new Error(`Invalid reference_source '${value}' for step '${stepName}': ` +
|
|
1276
|
+
`expected one of: ${REFERENCE_SOURCES.join(', ')}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (value === 'repo') {
|
|
1279
|
+
referenceSource = 'repo';
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1199
1282
|
const { width, height } = parseRenderSize(String(this.getInput(inputs, 'render_size', DEFAULT_RENDER_SIZE)));
|
|
1200
1283
|
const scale = parseScale(this.getInput(inputs, 'scale', DEFAULT_SCALE));
|
|
1201
1284
|
const config = { packagePath, target, width, height, scale };
|
|
1202
1285
|
if (packageSource !== undefined) {
|
|
1203
1286
|
config.packageSource = packageSource;
|
|
1204
1287
|
}
|
|
1288
|
+
if (referenceSource !== undefined) {
|
|
1289
|
+
config.referenceSource = referenceSource;
|
|
1290
|
+
}
|
|
1205
1291
|
const script = generateRenderScript(config);
|
|
1206
1292
|
return this.createNodeStep(script, stepName);
|
|
1207
1293
|
}
|
|
@@ -319,10 +319,13 @@ describe('registry integration', () => {
|
|
|
319
319
|
expect(Object.keys(inputs).sort()).toEqual([
|
|
320
320
|
'package_path',
|
|
321
321
|
'package_source',
|
|
322
|
+
'reference_source',
|
|
322
323
|
'render_size',
|
|
323
324
|
'scale',
|
|
324
325
|
'target',
|
|
325
326
|
]);
|
|
327
|
+
expect(inputs.reference_source.required).toBe(false);
|
|
328
|
+
expect(inputs.reference_source.default).toBe('inputs');
|
|
326
329
|
// package_path/target are enforced by the executor in repo mode (the
|
|
327
330
|
// default); a package_source:inputs config is valid without either, so
|
|
328
331
|
// neither is unconditionally required at the schema level.
|
|
@@ -528,6 +531,7 @@ describe('render script runtime', () => {
|
|
|
528
531
|
error: null,
|
|
529
532
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
530
533
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
534
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
531
535
|
},
|
|
532
536
|
{
|
|
533
537
|
screen: 'SettingsView',
|
|
@@ -535,6 +539,7 @@ describe('render script runtime', () => {
|
|
|
535
539
|
error: null,
|
|
536
540
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
537
541
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
542
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
538
543
|
},
|
|
539
544
|
],
|
|
540
545
|
});
|
|
@@ -644,6 +649,17 @@ describe('render script runtime', () => {
|
|
|
644
649
|
expect(settings.error).toBeNull();
|
|
645
650
|
expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
|
|
646
651
|
});
|
|
652
|
+
test('emits a source_digest content key on a rendered screen entry', async () => {
|
|
653
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
654
|
+
writeReference(project, 'home.png');
|
|
655
|
+
// A committed package file so the digest is over real package source.
|
|
656
|
+
writeFileSync(join(project.packageDir, 'Package.swift'), '// swift-tools-version:5.9\nimport PackageDescription\n');
|
|
657
|
+
const run = runScript(project, await buildScript(project));
|
|
658
|
+
expect(run.status).toBe(0);
|
|
659
|
+
const home = readResult(project).screens[0];
|
|
660
|
+
expect(home.status).toBe('rendered');
|
|
661
|
+
expect(home.source_digest).toMatch(/^[0-9a-f]{64}$/);
|
|
662
|
+
});
|
|
647
663
|
test('a reference that is a directory fails only that screen', async () => {
|
|
648
664
|
const project = makeProject({
|
|
649
665
|
screens: { HomeView: 'home-dir.png', SettingsView: 'settings.png' },
|
|
@@ -913,6 +929,7 @@ describe('render script runtime (package_source: repo)', () => {
|
|
|
913
929
|
error: null,
|
|
914
930
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
915
931
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
932
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
916
933
|
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
917
934
|
dimensions: {
|
|
918
935
|
rendered_px: [320, 640],
|
|
@@ -1092,6 +1109,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1092
1109
|
error: null,
|
|
1093
1110
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
1094
1111
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
1112
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
1095
1113
|
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
1096
1114
|
dimensions: {
|
|
1097
1115
|
rendered_px: [320, 640],
|
|
@@ -1105,6 +1123,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1105
1123
|
error: null,
|
|
1106
1124
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
1107
1125
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
1126
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
1108
1127
|
aligned_image_path: 'ui-fidelity/aligned/SettingsView.png',
|
|
1109
1128
|
dimensions: {
|
|
1110
1129
|
rendered_px: [320, 640],
|
|
@@ -1509,4 +1528,56 @@ describe('committed fixture package', () => {
|
|
|
1509
1528
|
expectNoAbsolutePathLeaks(project, result);
|
|
1510
1529
|
}, 600_000);
|
|
1511
1530
|
});
|
|
1531
|
+
describe('reference_source: repo', () => {
|
|
1532
|
+
test('rejects an unknown reference_source value', async () => {
|
|
1533
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
1534
|
+
await expect(executor.execute({ package_path: './pkg', target: 'MyViews', reference_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/reference_source/);
|
|
1535
|
+
});
|
|
1536
|
+
test('omitting reference_source bakes no referenceSource into the script config', async () => {
|
|
1537
|
+
const script = await buildScript(makeProject());
|
|
1538
|
+
const configLine = script
|
|
1539
|
+
.split('\n')
|
|
1540
|
+
.find((l) => l.startsWith('var CONFIG = '));
|
|
1541
|
+
expect(configLine).toBeDefined();
|
|
1542
|
+
expect(configLine).not.toContain('referenceSource');
|
|
1543
|
+
});
|
|
1544
|
+
test('reads each screen reference from its repo-relative path in the checkout', async () => {
|
|
1545
|
+
const project = makeProject({
|
|
1546
|
+
screens: { HomeView: 'refs/home.png', SettingsView: 'design/settings.png' },
|
|
1547
|
+
});
|
|
1548
|
+
// Committed references live at repo-relative paths (resolved against cwd),
|
|
1549
|
+
// NOT in .ci/inputs.
|
|
1550
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
1551
|
+
mkdirSync(join(project.dir, 'design'), { recursive: true });
|
|
1552
|
+
writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
|
|
1553
|
+
writeFileSync(join(project.dir, 'design', 'settings.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:settings')]));
|
|
1554
|
+
const script = await buildScript(project, { reference_source: 'repo' });
|
|
1555
|
+
const run = runScript(project, script);
|
|
1556
|
+
expect(run.status).toBe(0);
|
|
1557
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
|
|
1558
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(true);
|
|
1559
|
+
const result = readResult(project);
|
|
1560
|
+
expect(result.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
|
|
1561
|
+
expect(result.screens.map((s) => s.reference_image_path)).toEqual([
|
|
1562
|
+
'ui-fidelity/references/HomeView.png',
|
|
1563
|
+
'ui-fidelity/references/SettingsView.png',
|
|
1564
|
+
]);
|
|
1565
|
+
});
|
|
1566
|
+
test('fails one screen when its committed reference is missing, renders the rest', async () => {
|
|
1567
|
+
const project = makeProject({
|
|
1568
|
+
screens: { HomeView: 'refs/home.png', GhostView: 'refs/ghost.png' },
|
|
1569
|
+
});
|
|
1570
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
1571
|
+
writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
|
|
1572
|
+
// refs/ghost.png intentionally absent.
|
|
1573
|
+
const run = runScript(project, await buildScript(project, { reference_source: 'repo' }));
|
|
1574
|
+
expect(run.status).not.toBe(0);
|
|
1575
|
+
const result = readResult(project);
|
|
1576
|
+
const home = result.screens.find((s) => s.screen === 'HomeView');
|
|
1577
|
+
const ghost = result.screens.find((s) => s.screen === 'GhostView');
|
|
1578
|
+
expect(home?.status).toBe('rendered');
|
|
1579
|
+
expect(ghost?.status).toBe('render_failed');
|
|
1580
|
+
expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1512
1583
|
//# sourceMappingURL=ui-fidelity-render.test.js.map
|