@invarn/cibuild 2.2.6 → 2.2.7

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.
@@ -63,6 +63,10 @@ export const DEFAULT_SCALE = 2;
63
63
  export const PACKAGE_SOURCES = ['repo', 'inputs'];
64
64
  /** Default package source: the package lives in the repo checkout. */
65
65
  export const DEFAULT_PACKAGE_SOURCE = 'repo';
66
+ /** Valid values for the reference_source input. */
67
+ export const REFERENCE_SOURCES = ['repo', 'inputs'];
68
+ /** Default reference source: references arrive as run inputs (v1 behaviour). */
69
+ export const DEFAULT_REFERENCE_SOURCE = 'inputs';
66
70
  /** Basename of the shipped package archive inside the run-inputs directory. */
67
71
  export const PACKAGE_ARCHIVE_BASENAME = 'package.tar.gz';
68
72
  /**
@@ -1112,6 +1116,41 @@ function buildPackageSourceMain() {
1112
1116
  return main;
1113
1117
  }
1114
1118
  const RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE = buildPackageSourceMain();
1119
+ /**
1120
+ * Patches the reference-sourcing block to read each screen's reference from a
1121
+ * repo-relative path (resolved against the checkout) instead of a plain
1122
+ * basename under `.ci/inputs/`. Applied on top of EITHER base main (v1 or the
1123
+ * package_source variant) — the package patches never touch this block, so the
1124
+ * anchor is present and unique in both. The downstream stat/copy logic is
1125
+ * unchanged; only how `source` is computed differs.
1126
+ */
1127
+ function buildReferenceRepoMain(base) {
1128
+ return replaceOnce(base, 'var basename = params.screens[entry.screen];\n' +
1129
+ ' if (\n' +
1130
+ " typeof basename !== 'string' || basename === '' ||\n" +
1131
+ " basename === '.' || basename === '..' ||\n" +
1132
+ ' basename !== path.basename(basename)\n' +
1133
+ ' ) {\n' +
1134
+ ' setError(\n' +
1135
+ ' entry,\n' +
1136
+ " 'REFERENCE_MISSING',\n" +
1137
+ " 'reference for ' + entry.screen + ' must be a plain file basename inside ' +\n" +
1138
+ " inputsDir + ', got: ' + JSON.stringify(basename)\n" +
1139
+ ' );\n' +
1140
+ ' return;\n' +
1141
+ ' }\n' +
1142
+ ' var source = path.join(inputsDir, basename);', 'var refPath = params.screens[entry.screen];\n' +
1143
+ " if (typeof refPath !== 'string' || refPath === '') {\n" +
1144
+ ' setError(\n' +
1145
+ ' entry,\n' +
1146
+ " 'REFERENCE_MISSING',\n" +
1147
+ " 'reference for ' + entry.screen + ' must be a non-empty repo-relative path, got: ' +\n" +
1148
+ ' JSON.stringify(refPath)\n' +
1149
+ ' );\n' +
1150
+ ' return;\n' +
1151
+ ' }\n' +
1152
+ ' var source = path.resolve(refPath);');
1153
+ }
1115
1154
  /**
1116
1155
  * Evaluates the embedded generator source and returns the real functions, so
1117
1156
  * unit tests exercise exactly the code that ships inside the runtime script.
@@ -1133,14 +1172,20 @@ export function getRenderScriptInternals() {
1133
1172
  * unpatched v1 runtime is used).
1134
1173
  */
1135
1174
  export function generateRenderScript(config) {
1175
+ let main = config.packageSource === undefined
1176
+ ? RENDER_SCRIPT_MAIN
1177
+ : RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
1178
+ // Reference-from-repo is an independent dimension layered on top of whichever
1179
+ // package base applies. Absent → byte-identical reference block (v1).
1180
+ if (config.referenceSource === 'repo') {
1181
+ main = buildReferenceRepoMain(main);
1182
+ }
1136
1183
  return [
1137
1184
  "'use strict';",
1138
1185
  '// ui-fidelity-render runtime script (generated by cibuild at YAML conversion time)',
1139
1186
  'var CONFIG = ' + JSON.stringify(config) + ';',
1140
1187
  RENDER_SCRIPT_GENERATORS,
1141
- config.packageSource === undefined
1142
- ? RENDER_SCRIPT_MAIN
1143
- : RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE,
1188
+ main,
1144
1189
  ].join('\n');
1145
1190
  }
1146
1191
  /**
@@ -1196,12 +1241,30 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
1196
1241
  throw new Error(`Invalid target '${target}' for step '${stepName}': ` +
1197
1242
  'must be a Swift module identifier (letters, digits, underscores)');
1198
1243
  }
1244
+ // reference_source mirrors package_source: validate when provided, but only
1245
+ // record (and patch the script for) the non-default "repo" so omitting it —
1246
+ // or setting the default "inputs" — keeps the v1 reference block intact.
1247
+ const rawReferenceSource = this.getInput(inputs, 'reference_source', undefined);
1248
+ let referenceSource;
1249
+ if (rawReferenceSource !== undefined) {
1250
+ const value = String(rawReferenceSource);
1251
+ if (!REFERENCE_SOURCES.includes(value)) {
1252
+ throw new Error(`Invalid reference_source '${value}' for step '${stepName}': ` +
1253
+ `expected one of: ${REFERENCE_SOURCES.join(', ')}`);
1254
+ }
1255
+ if (value === 'repo') {
1256
+ referenceSource = 'repo';
1257
+ }
1258
+ }
1199
1259
  const { width, height } = parseRenderSize(String(this.getInput(inputs, 'render_size', DEFAULT_RENDER_SIZE)));
1200
1260
  const scale = parseScale(this.getInput(inputs, 'scale', DEFAULT_SCALE));
1201
1261
  const config = { packagePath, target, width, height, scale };
1202
1262
  if (packageSource !== undefined) {
1203
1263
  config.packageSource = packageSource;
1204
1264
  }
1265
+ if (referenceSource !== undefined) {
1266
+ config.referenceSource = referenceSource;
1267
+ }
1205
1268
  const script = generateRenderScript(config);
1206
1269
  return this.createNodeStep(script, stepName);
1207
1270
  }
@@ -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.
@@ -1509,4 +1512,56 @@ describe('committed fixture package', () => {
1509
1512
  expectNoAbsolutePathLeaks(project, result);
1510
1513
  }, 600_000);
1511
1514
  });
1515
+ describe('reference_source: repo', () => {
1516
+ test('rejects an unknown reference_source value', async () => {
1517
+ const executor = new UiFidelityRenderStepExecutor();
1518
+ await expect(executor.execute({ package_path: './pkg', target: 'MyViews', reference_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/reference_source/);
1519
+ });
1520
+ test('omitting reference_source bakes no referenceSource into the script config', async () => {
1521
+ const script = await buildScript(makeProject());
1522
+ const configLine = script
1523
+ .split('\n')
1524
+ .find((l) => l.startsWith('var CONFIG = '));
1525
+ expect(configLine).toBeDefined();
1526
+ expect(configLine).not.toContain('referenceSource');
1527
+ });
1528
+ test('reads each screen reference from its repo-relative path in the checkout', async () => {
1529
+ const project = makeProject({
1530
+ screens: { HomeView: 'refs/home.png', SettingsView: 'design/settings.png' },
1531
+ });
1532
+ // Committed references live at repo-relative paths (resolved against cwd),
1533
+ // NOT in .ci/inputs.
1534
+ mkdirSync(join(project.dir, 'refs'), { recursive: true });
1535
+ mkdirSync(join(project.dir, 'design'), { recursive: true });
1536
+ writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
1537
+ writeFileSync(join(project.dir, 'design', 'settings.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:settings')]));
1538
+ const script = await buildScript(project, { reference_source: 'repo' });
1539
+ const run = runScript(project, script);
1540
+ expect(run.status).toBe(0);
1541
+ expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
1542
+ expect(isPng(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(true);
1543
+ const result = readResult(project);
1544
+ expect(result.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
1545
+ expect(result.screens.map((s) => s.reference_image_path)).toEqual([
1546
+ 'ui-fidelity/references/HomeView.png',
1547
+ 'ui-fidelity/references/SettingsView.png',
1548
+ ]);
1549
+ });
1550
+ test('fails one screen when its committed reference is missing, renders the rest', async () => {
1551
+ const project = makeProject({
1552
+ screens: { HomeView: 'refs/home.png', GhostView: 'refs/ghost.png' },
1553
+ });
1554
+ mkdirSync(join(project.dir, 'refs'), { recursive: true });
1555
+ writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
1556
+ // refs/ghost.png intentionally absent.
1557
+ const run = runScript(project, await buildScript(project, { reference_source: 'repo' }));
1558
+ expect(run.status).not.toBe(0);
1559
+ const result = readResult(project);
1560
+ const home = result.screens.find((s) => s.screen === 'HomeView');
1561
+ const ghost = result.screens.find((s) => s.screen === 'GhostView');
1562
+ expect(home?.status).toBe('rendered');
1563
+ expect(ghost?.status).toBe('render_failed');
1564
+ expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
1565
+ });
1566
+ });
1512
1567
  //# sourceMappingURL=ui-fidelity-render.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",