@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.
Files changed (39) hide show
  1. package/dist/cli.cjs +54 -37
  2. package/dist/src/cli.js +8 -0
  3. package/dist/src/commands/fidelity-trim.d.ts +13 -0
  4. package/dist/src/commands/fidelity-trim.d.ts.map +1 -0
  5. package/dist/src/commands/fidelity-trim.js +21 -0
  6. package/dist/src/commands/fidelity-trim.test.d.ts +8 -0
  7. package/dist/src/commands/fidelity-trim.test.d.ts.map +1 -0
  8. package/dist/src/commands/fidelity-trim.test.js +79 -0
  9. package/dist/src/yaml/steps/render-post-processor.d.ts +52 -32
  10. package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -1
  11. package/dist/src/yaml/steps/render-post-processor.js +104 -205
  12. package/dist/src/yaml/steps/render-post-processor.test.d.ts +7 -10
  13. package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -1
  14. package/dist/src/yaml/steps/render-post-processor.test.js +80 -183
  15. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +28 -0
  16. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +1 -0
  17. package/dist/src/yaml/steps/trim-align-corpus.test.js +383 -0
  18. package/dist/src/yaml/steps/trim-align.d.ts +85 -0
  19. package/dist/src/yaml/steps/trim-align.d.ts.map +1 -0
  20. package/dist/src/yaml/steps/trim-align.js +348 -0
  21. package/dist/src/yaml/steps/trim-align.test.d.ts +11 -0
  22. package/dist/src/yaml/steps/trim-align.test.d.ts.map +1 -0
  23. package/dist/src/yaml/steps/trim-align.test.js +258 -0
  24. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +1 -1
  25. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
  26. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +9 -0
  27. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +4 -3
  28. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
  29. package/dist/src/yaml/steps/ui-fidelity-preview.js +9 -0
  30. package/dist/src/yaml/steps/ui-fidelity-preview.test.js +13 -3
  31. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +16 -5
  32. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
  33. package/dist/src/yaml/steps/ui-fidelity-render-android.js +190 -26
  34. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +165 -10
  35. package/dist/src/yaml/steps/ui-fidelity-render.d.ts +20 -4
  36. package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
  37. package/dist/src/yaml/steps/ui-fidelity-render.js +186 -10
  38. package/dist/src/yaml/steps/ui-fidelity-render.test.js +180 -22
  39. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { spawn, spawnSync } from 'node:child_process';
2
2
  import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
3
  import { gzipSync } from 'node:zlib';
4
4
  import { tmpdir } from 'node:os';
@@ -57,14 +57,14 @@ const FAKE_GRADLEW_LINES = [
57
57
  'done',
58
58
  'exit 0',
59
59
  ];
60
- // Fake `swift`, providing only the script-interpreter branch the shared trim
61
- // stage uses: `swift <Trim.swift> <in> <out> <aligned> <ref>`. Overwrites
62
- // <out> with a "trimmed:<in basename>" PNG, writes an "aligned:<in basename>"
63
- // PNG, and emits a fixed TRIMDIMS line. FAKE_TRIM_FAIL exercises warn-and-continue.
64
- const FAKE_SWIFT_LINES = [
60
+ // Fake `ci`, providing only the branch the shared trim stage uses:
61
+ // `ci fidelity-trim <in> <out> <aligned> <ref>`. Overwrites <out> with a
62
+ // "trimmed:<in basename>" PNG, writes an "aligned:<in basename>" PNG, and
63
+ // emits a fixed TRIMDIMS line. FAKE_TRIM_FAIL exercises warn-and-continue.
64
+ const FAKE_CI_LINES = [
65
65
  '#!/bin/bash',
66
66
  'case "$1" in',
67
- ' *.swift)',
67
+ ' fidelity-trim)',
68
68
  ' if [ -n "$FAKE_TRIM_FAIL" ]; then echo "trim: simulated failure" >&2; exit 1; fi',
69
69
  ' in_png="$2"; out_png="$3"; aligned_png="$4"',
70
70
  " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out_png\"",
@@ -84,9 +84,9 @@ function makeProject(params) {
84
84
  mkdirSync(join(dir, '.ci', 'inputs'), { recursive: true });
85
85
  const binDir = join(dir, 'bin');
86
86
  mkdirSync(binDir, { recursive: true });
87
- const swiftPath = join(binDir, 'swift');
88
- writeFileSync(swiftPath, FAKE_SWIFT_LINES.join('\n') + '\n');
89
- chmodSync(swiftPath, 0o755);
87
+ const ciPath = join(binDir, 'ci');
88
+ writeFileSync(ciPath, FAKE_CI_LINES.join('\n') + '\n');
89
+ chmodSync(ciPath, 0o755);
90
90
  if (params !== undefined) {
91
91
  writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), typeof params === 'string' ? params : JSON.stringify(params));
92
92
  }
@@ -826,6 +826,161 @@ describe('package_source: repo', () => {
826
826
  expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
827
827
  });
828
828
  });
829
+ // Clone-free package delivery (fidelity-linux 03): the dispatched YAML carries
830
+ // a signed package_url; the runtime downloads it to .ci/inputs/package.tar.gz
831
+ // and proceeds byte-for-byte like inputs mode, resolving each screen's
832
+ // committed reference INSIDE the extracted package (repo-relative path minus
833
+ // the package_path prefix) — there is no checkout on a clone-free worker.
834
+ describe('package_source: url', () => {
835
+ /**
836
+ * Serve one tar.gz (or a 404) from a loopback HTTP server IN A CHILD
837
+ * PROCESS. runScript executes the generated script via spawnSync, which
838
+ * blocks this process's event loop — an in-process server would deadlock
839
+ * against the script's curl. The child prints its port once listening.
840
+ */
841
+ const SERVE_PACKAGE_SRC = [
842
+ "var http = require('http');",
843
+ "var fs = require('fs');",
844
+ 'var filePath = process.argv[1];',
845
+ 'var server = http.createServer(function (req, res) {',
846
+ " if (filePath === '-') { res.statusCode = 404; res.end('gone'); return; }",
847
+ " res.setHeader('content-type', 'application/gzip');",
848
+ ' res.end(fs.readFileSync(filePath));',
849
+ '});',
850
+ "server.listen(0, '127.0.0.1', function () {",
851
+ " console.log('PORT ' + server.address().port);",
852
+ '});',
853
+ ].join('\n');
854
+ async function servePackage(archive) {
855
+ let filePath = '-';
856
+ if (archive !== null) {
857
+ const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-url-serve-'));
858
+ tempDirs.push(dir);
859
+ filePath = join(dir, 'package.tar.gz');
860
+ writeFileSync(filePath, archive);
861
+ }
862
+ const child = spawn('node', ['-e', SERVE_PACKAGE_SRC, filePath], {
863
+ stdio: ['ignore', 'pipe', 'inherit'],
864
+ });
865
+ const port = await new Promise((resolvePort, rejectPort) => {
866
+ let buffered = '';
867
+ const timer = setTimeout(() => rejectPort(new Error('package server did not start')), 10_000);
868
+ child.stdout.on('data', (chunk) => {
869
+ buffered += chunk.toString();
870
+ const match = /PORT (\d+)/.exec(buffered);
871
+ if (match) {
872
+ clearTimeout(timer);
873
+ resolvePort(Number(match[1]));
874
+ }
875
+ });
876
+ child.on('exit', () => rejectPort(new Error('package server exited early')));
877
+ });
878
+ return {
879
+ url: `http://127.0.0.1:${port}/api/fidelity/packages/blob1?exp=1&sig=ab`,
880
+ close: () => new Promise((resolveClose) => {
881
+ child.once('exit', () => resolveClose(undefined));
882
+ child.kill('SIGKILL');
883
+ }),
884
+ };
885
+ }
886
+ /** Archive shaped like the dashboard's assembly: package-dir contents at the
887
+ * archive root, committed references under references/. */
888
+ function packagedProject() {
889
+ return makeTarGz([
890
+ ...gradleProjectEntries(''),
891
+ { name: 'references/x.png', content: Buffer.concat([PNG_MAGIC, Buffer.from('ref:url-x')]) },
892
+ ]);
893
+ }
894
+ test('package_source url requires package_url and package_path', async () => {
895
+ const executor = new UiFidelityRenderAndroidStepExecutor();
896
+ await expect(executor.execute({ package_source: 'url', package_path: 'android-app' }, {}, testConfig)).rejects.toThrow(/package_url/);
897
+ await expect(executor.execute({ package_source: 'url', package_url: 'https://x.test/p' }, {}, testConfig)).rejects.toThrow(/package_path/);
898
+ });
899
+ test('rejects reference_source alongside package_source url (URL mode owns references)', async () => {
900
+ const executor = new UiFidelityRenderAndroidStepExecutor();
901
+ await expect(executor.execute({
902
+ package_source: 'url',
903
+ package_url: 'https://x.test/p',
904
+ package_path: 'android-app',
905
+ reference_source: 'repo',
906
+ }, {}, testConfig)).rejects.toThrow(/reference_source/);
907
+ });
908
+ test('url mode requires curl on the runner (validation requirement)', () => {
909
+ const executor = new UiFidelityRenderAndroidStepExecutor();
910
+ const urlReqs = executor.getValidationRequirements({ package_source: 'url' }, {}, testConfig);
911
+ expect(urlReqs.some((r) => r.name === 'curl')).toBe(true);
912
+ // Default inputs mode keeps tar only — no new requirement for existing runs.
913
+ const defaultReqs = executor.getValidationRequirements({}, {}, testConfig);
914
+ expect(defaultReqs.some((r) => r.name === 'curl')).toBe(false);
915
+ });
916
+ test('downloads the package, renders, and resolves references inside it', async () => {
917
+ const project = makeProject({ screens: { XProof: 'android-app/references/x.png' } });
918
+ const served = await servePackage(packagedProject());
919
+ try {
920
+ const run = runScript(project, await buildScript({
921
+ package_source: 'url',
922
+ package_url: served.url,
923
+ package_path: 'android-app',
924
+ }), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
925
+ expect(run.status).toBe(0);
926
+ const doc = readResult(project);
927
+ expect(doc.package_source).toBe('url');
928
+ const screen = doc.screens.find((s) => s.screen === 'XProof');
929
+ expect(screen?.status).toBe('rendered');
930
+ expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
931
+ expect(isPng(artifact(project, 'ui-fidelity/references/XProof.png'))).toBe(true);
932
+ }
933
+ finally {
934
+ await served.close();
935
+ }
936
+ });
937
+ test('a failed download is one structured build error, not per-screen noise', async () => {
938
+ const project = makeProject({ screens: { XProof: 'android-app/references/x.png' } });
939
+ const served = await servePackage(null); // 404
940
+ try {
941
+ const run = runScript(project, await buildScript({
942
+ package_source: 'url',
943
+ package_url: served.url,
944
+ package_path: 'android-app',
945
+ }));
946
+ expect(run.status).not.toBe(0);
947
+ const doc = readResult(project);
948
+ expect(doc.error?.code).toBe('ANDROID_PACKAGE_DOWNLOAD_FAILED');
949
+ const screen = doc.screens.find((s) => s.screen === 'XProof');
950
+ // The failure is the build's, not the screen's — no bogus
951
+ // REFERENCE_MISSING masking the real cause.
952
+ expect(screen?.error).toBeNull();
953
+ }
954
+ finally {
955
+ await served.close();
956
+ }
957
+ });
958
+ test('a reference outside the package directory fails that screen only', async () => {
959
+ const project = makeProject({
960
+ screens: {
961
+ XProof: 'android-app/references/x.png',
962
+ Stray: 'design/elsewhere/stray.png',
963
+ },
964
+ });
965
+ const served = await servePackage(packagedProject());
966
+ try {
967
+ const run = runScript(project, await buildScript({
968
+ package_source: 'url',
969
+ package_url: served.url,
970
+ package_path: 'android-app',
971
+ }), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
972
+ expect(run.status).not.toBe(0);
973
+ const doc = readResult(project);
974
+ expect(doc.screens.find((s) => s.screen === 'XProof')?.status).toBe('rendered');
975
+ const stray = doc.screens.find((s) => s.screen === 'Stray');
976
+ expect(stray?.status).toBe('render_failed');
977
+ expect(stray?.error?.code).toBe('REFERENCE_MISSING');
978
+ }
979
+ finally {
980
+ await served.close();
981
+ }
982
+ });
983
+ });
829
984
  describe('reference_source: repo', () => {
830
985
  test('rejects an unknown reference_source value', async () => {
831
986
  const executor = new UiFidelityRenderAndroidStepExecutor();
@@ -71,8 +71,17 @@ export interface UiFidelityRenderInputs {
71
71
  * behavior; omitting package_source generates a byte-identical script).
72
72
  * - "inputs": the package ships as .ci/inputs/package.tar.gz and is
73
73
  * extracted, validated, and built from a scratch directory at runtime.
74
+ * - "url" (fidelity-linux 03): the same package.tar.gz is downloaded from
75
+ * the signed package_url into the run-inputs directory, then extracted
76
+ * and built exactly like inputs mode — the clone-free delivery for
77
+ * triggered runs. References resolve INSIDE the extracted package.
74
78
  */
75
- package_source?: 'repo' | 'inputs';
79
+ package_source?: 'repo' | 'inputs' | 'url';
80
+ /**
81
+ * Signed download URL for package.tar.gz. Required with package_source
82
+ * "url"; ignored otherwise.
83
+ */
84
+ package_url?: string;
76
85
  /**
77
86
  * Path to the user's SwiftPM package (the package that defines the
78
87
  * screens). Required in repo mode; ignored with package_source "inputs".
@@ -110,7 +119,7 @@ export declare const DEFAULT_RENDER_SIZE = "393x852";
110
119
  /** Default display scale (@2x), matching how references are typically exported. */
111
120
  export declare const DEFAULT_SCALE = 2;
112
121
  /** Valid values for the package_source input. */
113
- export declare const PACKAGE_SOURCES: readonly ["repo", "inputs"];
122
+ export declare const PACKAGE_SOURCES: readonly ["repo", "inputs", "url"];
114
123
  export type PackageSource = (typeof PACKAGE_SOURCES)[number];
115
124
  /** Default package source: the package lives in the repo checkout. */
116
125
  export declare const DEFAULT_PACKAGE_SOURCE: PackageSource;
@@ -157,14 +166,21 @@ export declare function parseScale(value: number | string): number;
157
166
  * `error` (null, or `{ code, message }` for per-build package failures).
158
167
  */
159
168
  export interface RenderScriptConfig {
160
- /** null only with packageSource "inputs", where the package is shipped. */
169
+ /** null only with packageSource "inputs", where the package is shipped.
170
+ * With packageSource "url" it carries the repo-relative package directory
171
+ * committed reference paths are resolved under (never a local path). */
161
172
  packagePath: string | null;
162
- /** null only with packageSource "inputs", where it may be discovered. */
173
+ /** null with packageSource "inputs"/"url" when it is to be discovered. */
163
174
  target: string | null;
164
175
  width: number;
165
176
  height: number;
166
177
  scale: number;
167
178
  packageSource?: PackageSource;
179
+ /**
180
+ * Signed download URL for package.tar.gz. Present (and required) only with
181
+ * packageSource "url"; ignored otherwise.
182
+ */
183
+ packageUrl?: string;
168
184
  /**
169
185
  * Only present (and only ever "repo") when reference_source was explicitly set
170
186
  * to "repo". When absent, references are read from `.ci/inputs/<basename>` and
@@ -1 +1 @@
1
- {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
@@ -71,7 +71,7 @@ export const DEFAULT_RENDER_SIZE = '393x852';
71
71
  /** Default display scale (@2x), matching how references are typically exported. */
72
72
  export const DEFAULT_SCALE = 2;
73
73
  /** Valid values for the package_source input. */
74
- export const PACKAGE_SOURCES = ['repo', 'inputs'];
74
+ export const PACKAGE_SOURCES = ['repo', 'inputs', 'url'];
75
75
  /** Default package source: the package lives in the repo checkout. */
76
76
  export const DEFAULT_PACKAGE_SOURCE = 'repo';
77
77
  /** Valid values for the reference_source input. */
@@ -1250,7 +1250,7 @@ function buildPackageSourceMain() {
1250
1250
  // crop each rendered PNG to its content bounding box right after it renders.
1251
1251
  main = replaceOnce(main, 'function renderWithHarness(renderable, packagePath) {', RENDER_SCRIPT_TRIM_STAGE + '\nfunction renderWithHarness(renderable, packagePath) {');
1252
1252
  main = replaceOnce(main, " log('rendered ' + entry.screen + ' -> ' + relative);", " log('rendered ' + entry.screen + ' -> ' + relative);\n" +
1253
- ' trimRenderedImage(entry, outputPath, harnessDir);');
1253
+ ' trimRenderedImage(entry, outputPath);');
1254
1254
  // Per-screen result entries gain the trimmed-render fields: the aligned
1255
1255
  // (reference-sized) image and the rendered/logical/reference dimensions.
1256
1256
  // They stay null until a screen renders and is trimmed.
@@ -1264,6 +1264,154 @@ function buildPackageSourceMain() {
1264
1264
  return main;
1265
1265
  }
1266
1266
  const RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE = buildPackageSourceMain();
1267
+ /**
1268
+ * Builds the runtime main for package_source "url" (fidelity-linux 03) by
1269
+ * patching the package_source variant. Three departures, all forced by the
1270
+ * same fact — a clone-free worker has no checkout:
1271
+ *
1272
+ * 1. The package is DOWNLOADED (curl, a runner requirement in this mode) to
1273
+ * `.ci/inputs/package.tar.gz` and extracted BEFORE the per-screen
1274
+ * reference stage, because committed references travel inside it. A failed
1275
+ * download is one structured per-build error, never per-screen noise.
1276
+ * 2. Each screen's reference is the committed repo-relative path resolved
1277
+ * INSIDE the extracted package: it must sit under packagePath, and the
1278
+ * remainder resolves against the extracted package root.
1279
+ * 3. The harness stage reuses the already-extracted root (no second extract);
1280
+ * cleanup mirrors the inputs path. Mirrors the Android step's url mode so
1281
+ * both platform kinds accept the same dispatched inputs.
1282
+ */
1283
+ function buildPackageUrlMain() {
1284
+ let main = RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
1285
+ // Download + extract FIRST — the reference stage below reads from inside
1286
+ // the extracted package.
1287
+ main = replaceOnce(main, ' // 2. Per-screen validation + reference copies. Each screen gets its OWN\n' +
1288
+ ' // copy named by screen, even when two screens share a reference\n' +
1289
+ ' // basename.\n' +
1290
+ ' currentEntries.forEach(function (entry) {', ' // 2a. Download + extract the shipped package FIRST (URL mode): committed\n' +
1291
+ ' // references live inside it, so the per-screen reference copies below\n' +
1292
+ ' // need the extracted package root. A failed download or an invalid\n' +
1293
+ ' // archive is ONE structured per-build error; per-screen reference\n' +
1294
+ ' // work then stops cleanly (no bogus REFERENCE_MISSING noise).\n' +
1295
+ ' var urlPackageRoot = null;\n' +
1296
+ ' (function () {\n' +
1297
+ ' try {\n' +
1298
+ ' fs.mkdirSync(inputsDir, { recursive: true });\n' +
1299
+ ' } catch (mkdirError) {\n' +
1300
+ ' // the download below reports the real failure\n' +
1301
+ ' }\n' +
1302
+ ' var dest = path.join(inputsDir, PACKAGE_ARCHIVE_NAME);\n' +
1303
+ " log('downloading render package');\n" +
1304
+ ' var download = cp.spawnSync(\n' +
1305
+ " 'curl',\n" +
1306
+ " ['-fsSL', '--retry', '2', '--max-time', '300', '-o', dest, CONFIG.packageUrl],\n" +
1307
+ " { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 }\n" +
1308
+ ' );\n' +
1309
+ ' if (download.error) {\n' +
1310
+ ' download.status = download.status == null ? 127 : download.status;\n' +
1311
+ " download.stderr = (download.stderr || '') + String(download.error.message || download.error);\n" +
1312
+ ' }\n' +
1313
+ ' if (download.status !== 0) {\n' +
1314
+ ' setBuildError(\n' +
1315
+ " 'PACKAGE_DOWNLOAD_FAILED',\n" +
1316
+ " 'render package download failed (curl exit ' + download.status + '):\\n' + outputTail(download)\n" +
1317
+ ' );\n' +
1318
+ ' return;\n' +
1319
+ ' }\n' +
1320
+ ' urlPackageRoot = prepareShippedPackage();\n' +
1321
+ ' })();\n' +
1322
+ '\n' +
1323
+ ' // 2. Per-screen validation + reference copies. Each screen gets its OWN\n' +
1324
+ ' // copy named by screen, even when two screens share a reference\n' +
1325
+ ' // basename.\n' +
1326
+ ' currentEntries.forEach(function (entry) {');
1327
+ // References resolve INSIDE the extracted package: repo-relative path minus
1328
+ // the packagePath prefix, against the extracted package root.
1329
+ main = replaceOnce(main, 'var basename = params.screens[entry.screen];\n' +
1330
+ ' if (\n' +
1331
+ " typeof basename !== 'string' || basename === '' ||\n" +
1332
+ " basename === '.' || basename === '..' ||\n" +
1333
+ ' basename !== path.basename(basename)\n' +
1334
+ ' ) {\n' +
1335
+ ' setError(\n' +
1336
+ ' entry,\n' +
1337
+ " 'REFERENCE_MISSING',\n" +
1338
+ " 'reference for ' + entry.screen + ' must be a plain file basename inside ' +\n" +
1339
+ " inputsDir + ', got: ' + JSON.stringify(basename)\n" +
1340
+ ' );\n' +
1341
+ ' return;\n' +
1342
+ ' }\n' +
1343
+ ' var source = path.join(inputsDir, basename);', 'var refPath = params.screens[entry.screen];\n' +
1344
+ " if (typeof refPath !== 'string' || refPath === '') {\n" +
1345
+ ' setError(\n' +
1346
+ ' entry,\n' +
1347
+ " 'REFERENCE_MISSING',\n" +
1348
+ " 'reference for ' + entry.screen + ' must be a non-empty repo-relative path, got: ' +\n" +
1349
+ ' JSON.stringify(refPath)\n' +
1350
+ ' );\n' +
1351
+ ' return;\n' +
1352
+ ' }\n' +
1353
+ ' if (urlPackageRoot === null) {\n' +
1354
+ ' // Package unavailable: the per-build error above carries the cause.\n' +
1355
+ ' return;\n' +
1356
+ ' }\n' +
1357
+ " var packagePrefix = CONFIG.packagePath.replace(/^\\.\\//, '').replace(/\\/+$/, '');\n" +
1358
+ " var normalizedRef = refPath.replace(/^\\.\\//, '');\n" +
1359
+ " if (normalizedRef.indexOf(packagePrefix + '/') !== 0) {\n" +
1360
+ ' setError(\n' +
1361
+ ' entry,\n' +
1362
+ " 'REFERENCE_MISSING',\n" +
1363
+ " 'reference for ' + entry.screen + ' must live inside the shipped package (' +\n" +
1364
+ " packagePrefix + '), got: ' + JSON.stringify(refPath)\n" +
1365
+ ' );\n' +
1366
+ ' return;\n' +
1367
+ ' }\n' +
1368
+ ' var source = path.join(urlPackageRoot, normalizedRef.slice(packagePrefix.length + 1));');
1369
+ // Harness stage: build from the package extracted in stage 2a — no second
1370
+ // extract. This variant only ever runs with packageSource "url".
1371
+ main = replaceOnce(main, ' // 3. Resolve the package to build from, then render the remaining\n' +
1372
+ ' // screens through the synthesized harness.\n' +
1373
+ ' var renderable = currentEntries.filter(function (entry) { return entry.error === null; });\n' +
1374
+ " if (CONFIG.packageSource === 'inputs') {\n" +
1375
+ ' try {\n' +
1376
+ ' var shippedRoot = prepareShippedPackage();\n' +
1377
+ ' if (shippedRoot !== null) {\n' +
1378
+ ' if (renderable.length > 0) {\n' +
1379
+ ' renderWithHarness(renderable, shippedRoot);\n' +
1380
+ ' } else if (currentEntries.length > 0) {\n' +
1381
+ " log('no renderable screens, skipping harness build');\n" +
1382
+ ' }\n' +
1383
+ ' }\n' +
1384
+ ' } finally {\n' +
1385
+ ' cleanupShippedPackage();\n' +
1386
+ ' }\n' +
1387
+ ' } else if (renderable.length > 0) {\n' +
1388
+ ' if (!fs.existsSync(resolvedPackagePath)) {\n' +
1389
+ ' currentEntries.forEach(function (entry) {\n' +
1390
+ " setError(entry, 'RENDER_UNSUPPORTED', 'package_path does not exist: ' + resolvedPackagePath);\n" +
1391
+ ' });\n' +
1392
+ ' } else {\n' +
1393
+ ' renderWithHarness(renderable, resolvedPackagePath);\n' +
1394
+ ' }\n' +
1395
+ ' } else if (currentEntries.length > 0) {\n' +
1396
+ " log('no renderable screens, skipping harness build');\n" +
1397
+ ' }', ' // 3. Render from the package downloaded + extracted in stage 2a. The\n' +
1398
+ ' // extraction already reported packaging mistakes as the per-build\n' +
1399
+ ' // error, so a null root here just skips the harness build.\n' +
1400
+ ' var renderable = currentEntries.filter(function (entry) { return entry.error === null; });\n' +
1401
+ ' try {\n' +
1402
+ ' if (urlPackageRoot !== null) {\n' +
1403
+ ' if (renderable.length > 0) {\n' +
1404
+ ' renderWithHarness(renderable, urlPackageRoot);\n' +
1405
+ ' } else if (currentEntries.length > 0) {\n' +
1406
+ " log('no renderable screens, skipping harness build');\n" +
1407
+ ' }\n' +
1408
+ ' }\n' +
1409
+ ' } finally {\n' +
1410
+ ' cleanupShippedPackage();\n' +
1411
+ ' }');
1412
+ return main;
1413
+ }
1414
+ const RENDER_SCRIPT_MAIN_URL = buildPackageUrlMain();
1267
1415
  /**
1268
1416
  * Patches the reference-sourcing block to read each screen's reference from a
1269
1417
  * repo-relative path (resolved against the checkout) instead of a plain
@@ -1322,7 +1470,9 @@ export function getRenderScriptInternals() {
1322
1470
  export function generateRenderScript(config) {
1323
1471
  let main = config.packageSource === undefined
1324
1472
  ? RENDER_SCRIPT_MAIN
1325
- : RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
1473
+ : config.packageSource === 'url'
1474
+ ? RENDER_SCRIPT_MAIN_URL
1475
+ : RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
1326
1476
  // Reference-from-repo is an independent dimension layered on top of whichever
1327
1477
  // package base applies. Absent → byte-identical reference block (v1).
1328
1478
  if (config.referenceSource === 'repo') {
@@ -1344,13 +1494,18 @@ export function generateRenderScript(config) {
1344
1494
  */
1345
1495
  export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
1346
1496
  getValidationRequirements(inputs, _env, _config) {
1347
- if (inputs && inputs.package_source === 'inputs') {
1348
- // The package ships as .ci/inputs/package.tar.gz at run time, so
1497
+ if (inputs && (inputs.package_source === 'inputs' || inputs.package_source === 'url')) {
1498
+ // The package ships as .ci/inputs/package.tar.gz at run time (uploaded
1499
+ // run input, or downloaded from the signed package_url in url mode), so
1349
1500
  // neither package_path nor target is required up front.
1350
- return [
1501
+ const requirements = [
1351
1502
  this.requireCommand('swift', 'Swift toolchain used to build and run the render harness', 'Install Xcode (or the Swift toolchain) on the runner'),
1352
1503
  this.requireCommand('tar', 'Archive tool used to extract the shipped package', 'Install tar on the runner'),
1353
1504
  ];
1505
+ if (inputs.package_source === 'url') {
1506
+ requirements.push(this.requireCommand('curl', 'Downloader used to fetch the render package by signed URL', 'Install curl on the runner'));
1507
+ }
1508
+ return requirements;
1354
1509
  }
1355
1510
  return [
1356
1511
  this.requireInput('package_path', inputs, 'Path to the SwiftPM package containing the screens'),
@@ -1375,10 +1530,21 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
1375
1530
  }
1376
1531
  let packagePath = null;
1377
1532
  let target = null;
1378
- if (packageSource === 'inputs') {
1379
- // package_path is ignored: the package arrives as a run input.
1380
- // target is optional and discovered from the shipped manifest when
1381
- // the package declares exactly one library product.
1533
+ let packageUrl;
1534
+ if (packageSource === 'inputs' || packageSource === 'url') {
1535
+ // package_path names no local directory here: inputs mode ignores it
1536
+ // (the package arrives as a run input); url mode requires it as the
1537
+ // repo-relative prefix committed reference paths sit under, inside the
1538
+ // downloaded package. target is optional and discovered from the
1539
+ // shipped manifest when the package declares exactly one library
1540
+ // product.
1541
+ if (packageSource === 'url') {
1542
+ packageUrl = String(this.getRequiredInput(inputs, 'package_url', stepName));
1543
+ if (!/^https?:\/\//.test(packageUrl)) {
1544
+ throw new Error(`Invalid package_url for step '${stepName}': expected an http(s) URL`);
1545
+ }
1546
+ packagePath = String(this.getRequiredInput(inputs, 'package_path', stepName));
1547
+ }
1382
1548
  const configuredTarget = this.getInput(inputs, 'target', undefined);
1383
1549
  target = configuredTarget === undefined ? null : String(configuredTarget);
1384
1550
  }
@@ -1401,6 +1567,13 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
1401
1567
  throw new Error(`Invalid reference_source '${value}' for step '${stepName}': ` +
1402
1568
  `expected one of: ${REFERENCE_SOURCES.join(', ')}`);
1403
1569
  }
1570
+ // URL mode owns reference sourcing (inside the extracted package) —
1571
+ // an explicit reference_source alongside it is a contradiction.
1572
+ if (packageSource === 'url') {
1573
+ throw new Error(`Invalid reference_source for step '${stepName}': ` +
1574
+ "package_source 'url' resolves references inside the downloaded " +
1575
+ 'package; omit reference_source');
1576
+ }
1404
1577
  if (value === 'repo') {
1405
1578
  referenceSource = 'repo';
1406
1579
  }
@@ -1411,6 +1584,9 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
1411
1584
  if (packageSource !== undefined) {
1412
1585
  config.packageSource = packageSource;
1413
1586
  }
1587
+ if (packageUrl !== undefined) {
1588
+ config.packageUrl = packageUrl;
1589
+ }
1414
1590
  if (referenceSource !== undefined) {
1415
1591
  config.referenceSource = referenceSource;
1416
1592
  }