@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
|
@@ -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 `
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
const
|
|
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
|
-
'
|
|
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
|
|
88
|
-
writeFileSync(
|
|
89
|
-
chmodSync(
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAIpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
//
|
|
1381
|
-
// the package
|
|
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
|
}
|