@invarn/cibuild 1.9.9 → 2.0.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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Node-kind step execution must ride a temp script FILE, never `node -e`.
3
+ *
4
+ * The released `ci` binary is pkg-built. pkg's bootstrap intercepts child
5
+ * spawns of `node` and re-executes the pkg binary itself — and that
6
+ * bootstrap cannot handle `-e`: it resolves it as an entrypoint path and
7
+ * dies with `Cannot find module '<cwd>/-e'` before the script runs. A
8
+ * real file path works under both a system node and the pkg bootstrap,
9
+ * exactly like bash steps already do via their temp-file pattern.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=runner-node-step.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner-node-step.test.d.ts","sourceRoot":"","sources":["../../src/runner-node-step.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Node-kind step execution must ride a temp script FILE, never `node -e`.
3
+ *
4
+ * The released `ci` binary is pkg-built. pkg's bootstrap intercepts child
5
+ * spawns of `node` and re-executes the pkg binary itself — and that
6
+ * bootstrap cannot handle `-e`: it resolves it as an entrypoint path and
7
+ * dies with `Cannot find module '<cwd>/-e'` before the script runs. A
8
+ * real file path works under both a system node and the pkg bootstrap,
9
+ * exactly like bash steps already do via their temp-file pattern.
10
+ */
11
+ import { describe, test, expect, afterEach } from '@jest/globals';
12
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { PipelineRunner, StepExecutionError } from './runner.js';
16
+ import { loadConfig } from './config.js';
17
+ function nodeStep(script) {
18
+ return {
19
+ id: 'step_test',
20
+ name: 'node-under-test',
21
+ kind: 'node',
22
+ script,
23
+ };
24
+ }
25
+ describe('PipelineRunner node-kind steps', () => {
26
+ let outDir = null;
27
+ afterEach(() => {
28
+ if (outDir)
29
+ rmSync(outDir, { recursive: true, force: true });
30
+ outDir = null;
31
+ });
32
+ test('executes the script from a file: argv[1] is a real path, never -e', async () => {
33
+ outDir = mkdtempSync(join(tmpdir(), 'cibuild-node-step-'));
34
+ const probePath = join(outDir, 'probe.json');
35
+ const runner = new PipelineRunner(loadConfig('/nonexistent/ci.config.json'));
36
+ await runner.runStep(nodeStep(`require('fs').writeFileSync(${JSON.stringify(probePath)}, JSON.stringify({` +
37
+ `argv1: process.argv[1] ?? null,` +
38
+ `filename: typeof __filename === 'string' ? __filename : null` +
39
+ `}))`));
40
+ const probe = JSON.parse(readFileSync(probePath, 'utf-8'));
41
+ // `node -e` leaves argv[1] undefined and has no __filename; a
42
+ // file-executed script has both, and neither may be the literal '-e'.
43
+ expect(probe.argv1).not.toBeNull();
44
+ expect(probe.argv1).not.toBe('-e');
45
+ expect(probe.filename).not.toBeNull();
46
+ expect(probe.filename).toMatch(/\.cjs$/);
47
+ });
48
+ test('propagates a non-zero exit as StepExecutionError', async () => {
49
+ const runner = new PipelineRunner(loadConfig('/nonexistent/ci.config.json'));
50
+ await expect(runner.runStep(nodeStep('process.exit(3)'))).rejects.toThrow(StepExecutionError);
51
+ });
52
+ test('cleans up the temp script file after the step exits', async () => {
53
+ outDir = mkdtempSync(join(tmpdir(), 'cibuild-node-step-'));
54
+ const probePath = join(outDir, 'probe.json');
55
+ const runner = new PipelineRunner(loadConfig('/nonexistent/ci.config.json'));
56
+ await runner.runStep(nodeStep(`require('fs').writeFileSync(${JSON.stringify(probePath)}, JSON.stringify({ filename: __filename }))`));
57
+ const { filename } = JSON.parse(readFileSync(probePath, 'utf-8'));
58
+ expect(() => readFileSync(filename)).toThrow();
59
+ });
60
+ });
61
+ //# sourceMappingURL=runner-node-step.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMjE,qBAAa,kBAAmB,SAAQ,KAAK;IAElC,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;gBAFhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAK1B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,QAAQ;IAE9B,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoJhE,OAAO,CAAC,cAAc;IAYtB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAwB7C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAczD,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA2CpG"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMjE,qBAAa,kBAAmB,SAAQ,KAAK;IAElC,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;gBAFhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAK1B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,QAAQ;IAE9B,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkKhE,OAAO,CAAC,cAAc;IAYtB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAwB7C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAczD,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA2CpG"}
@@ -121,8 +121,23 @@ export -f envman
121
121
  args = ["-e", step.script];
122
122
  break;
123
123
  case "node":
124
+ // Write the script to a file instead of passing it via -e: the
125
+ // released ci binary is pkg-built, and pkg's bootstrap intercepts
126
+ // child spawns of `node`, re-executing the pkg binary itself —
127
+ // which resolves `-e` as an entrypoint path and dies with
128
+ // `Cannot find module '<cwd>/-e'`. A real file path executes
129
+ // under both a system node and the pkg bootstrap.
124
130
  cmd = this.config.interpreters.node;
125
- args = ["-e", step.script];
131
+ try {
132
+ const tmpDir = mkdtempSync(join(tmpdir(), 'cibuild-script-'));
133
+ tmpScriptPath = join(tmpDir, `step-${step.id || 'script'}.cjs`);
134
+ writeFileSync(tmpScriptPath, step.script, 'utf-8');
135
+ }
136
+ catch (err) {
137
+ reject(new Error(`Failed to create temporary script file: ${err}`));
138
+ return;
139
+ }
140
+ args = [tmpScriptPath];
126
141
  break;
127
142
  default:
128
143
  reject(new Error(`Unknown step kind: ${step.kind}`));
@@ -10,7 +10,7 @@ export class PlatformDetectionError extends Error {
10
10
  }
11
11
  }
12
12
  // Platform-specific step names
13
- const MACOS_STEPS = new Set(['xcodebuild', 'xcode-test']);
13
+ const MACOS_STEPS = new Set(['xcodebuild', 'xcode-test', 'ui-fidelity-render']);
14
14
  const LINUX_STEPS = new Set(['gradle-build', 'android-lint', 'android-unit-test', 'set-java-version']);
15
15
  /**
16
16
  * PlatformDetector analyzes a YAML workflow and determines the target platform
@@ -207,6 +207,25 @@ describe('PlatformDetector', () => {
207
207
  const detector = new PlatformDetector(pipeline, pipeline.workflows.primary, 'primary');
208
208
  expect(detector.getPlatform()).toBe('macos');
209
209
  });
210
+ test('should detect macOS from ui-fidelity-render step', () => {
211
+ const pipeline = {
212
+ format_version: '4',
213
+ workflows: {
214
+ primary: {
215
+ steps: [
216
+ { 'git-clone@4.0.17': { inputs: {} } },
217
+ {
218
+ 'ui-fidelity-render@1.0.0': {
219
+ inputs: { package_path: './MyPackage', target: 'MyViews' },
220
+ },
221
+ },
222
+ ],
223
+ },
224
+ },
225
+ };
226
+ const detector = new PlatformDetector(pipeline, pipeline.workflows.primary, 'primary');
227
+ expect(detector.getPlatform()).toBe('macos');
228
+ });
210
229
  test('should detect macOS from xcode-test step', () => {
211
230
  const pipeline = {
212
231
  format_version: '4',
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+BH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAm1C7C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoCH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAi3C7C"}
@@ -24,6 +24,7 @@ import { BitriseSlackStepExecutor } from './bitrise-slack.js';
24
24
  import { BitriseApkInfoStepExecutor } from './bitrise-apk-info.js';
25
25
  import { GooglePlayDeployStepExecutor } from './google-play-deploy.js';
26
26
  import { AppStoreDeployStepExecutor } from './app-store-deploy.js';
27
+ import { UiFidelityRenderStepExecutor, DEFAULT_RENDER_SIZE, DEFAULT_SCALE, } from './ui-fidelity-render.js';
27
28
  /**
28
29
  * Initializes the step registry with all available steps
29
30
  * This function should be called once at application startup
@@ -757,6 +758,34 @@ export function initializeStepRegistry() {
757
758
  },
758
759
  },
759
760
  });
761
+ // UI fidelity — render SwiftUI screens to PNGs via ImageRenderer on macOS
762
+ registerStep('ui-fidelity-render', new UiFidelityRenderStepExecutor(), {
763
+ name: 'ui-fidelity-render',
764
+ description: 'Render parameterless SwiftUI views from a SwiftPM package to PNGs with ImageRenderer (no app build, no simulator)',
765
+ platform: 'ios',
766
+ inputs: {
767
+ package_path: {
768
+ description: "Path to the user's SwiftPM package containing the screens",
769
+ required: true,
770
+ inputType: 'path',
771
+ },
772
+ target: {
773
+ description: 'SPM library product to import in the render harness',
774
+ required: true,
775
+ inputType: 'text',
776
+ },
777
+ render_size: {
778
+ description: 'Render size in device points, "<width>x<height>"',
779
+ required: false,
780
+ default: DEFAULT_RENDER_SIZE,
781
+ },
782
+ scale: {
783
+ description: 'Display scale factor applied to the render',
784
+ required: false,
785
+ default: DEFAULT_SCALE,
786
+ },
787
+ },
788
+ });
760
789
  // Export xcarchive
761
790
  registerStep('export-xcarchive', new ExportXcarchiveStepExecutor(), {
762
791
  name: 'export-xcarchive',
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ui-fidelity-render step implementation.
3
+ *
4
+ * Renders parameterless SwiftUI views from a user's SwiftPM package to PNGs
5
+ * with SwiftUI's ImageRenderer on a macOS runner — no app build, no
6
+ * simulator. The step returns a self-contained node script that, at runtime:
7
+ *
8
+ * 1. Reads `.ci/inputs/params.json`
9
+ * ({ "screens": { "<ViewTypeName>": "<referenceFileBasename>" } });
10
+ * reference images live at `.ci/inputs/<basename>`.
11
+ * 2. Synthesizes a throwaway SwiftPM harness package that depends on the
12
+ * user's package and contains ONE executable target per screen, so a
13
+ * compile failure for one screen (unknown view type, unavailable init)
14
+ * never breaks the others. A screen-free probe target distinguishes
15
+ * "this screen is broken" from "the whole package does not build for
16
+ * macOS" (RENDER_UNSUPPORTED).
17
+ * 3. Builds and runs each per-screen executable, which constructs the view
18
+ * strictly as `<ViewTypeName>()`, renders at render_size x scale on the
19
+ * MainActor, and writes a PNG via CGImage + ImageIO.
20
+ * 4. Writes, inside the artifacts dir (${CIBUILD_ARTIFACTS_DIR:-.ci/artifacts}):
21
+ * - ui-fidelity/rendered/<Screen>.png
22
+ * - ui-fidelity/references/<Screen>.png (one copy per screen, even when
23
+ * two screens share a reference basename)
24
+ * - protocol-result.json (always written, even on partial/total failure;
25
+ * when params.json is absent/malformed the screens array is empty
26
+ * because screens cannot be enumerated)
27
+ * 5. Exits non-zero iff any screen is not "rendered".
28
+ *
29
+ * protocol-result.json contains relative paths only: image paths are relative
30
+ * to the artifacts dir, and error messages are sanitized so absolute runner
31
+ * paths (resolved package_path, harness temp dir, compiler diagnostics) are
32
+ * rewritten to relative paths or placeholders before they are stored.
33
+ */
34
+ import { BaseStepExecutor } from './base.js';
35
+ import type { StepDef, CIConfig } from '../../types.js';
36
+ import type { ValidationRequirement } from '../validation-types.js';
37
+ /**
38
+ * Inputs for the ui-fidelity-render step
39
+ */
40
+ export interface UiFidelityRenderInputs {
41
+ /** Path to the user's SwiftPM package (the package that defines the screens) */
42
+ package_path: string;
43
+ /** SPM library product/target to import in the render harness */
44
+ target: string;
45
+ /** Render size in device points, formatted "<width>x<height>" (default 393x852) */
46
+ render_size?: string;
47
+ /** Display scale factor applied to the render (default 2) */
48
+ scale?: number | string;
49
+ }
50
+ /**
51
+ * Default render size in device points. 393x852 is the iPhone-class portrait
52
+ * canvas shared by the iPhone 14 Pro / 15 / 16 — the most common modern
53
+ * iPhone logical resolution, so renders line up with design references by
54
+ * default.
55
+ */
56
+ export declare const DEFAULT_RENDER_SIZE = "393x852";
57
+ /** Default display scale (@2x), matching how references are typically exported. */
58
+ export declare const DEFAULT_SCALE = 2;
59
+ export interface RenderSize {
60
+ width: number;
61
+ height: number;
62
+ }
63
+ /**
64
+ * Parses a "<width>x<height>" render size string (device points).
65
+ * @throws Error when the value is malformed or non-positive
66
+ */
67
+ export declare function parseRenderSize(value: string): RenderSize;
68
+ /**
69
+ * Parses the scale input into a positive finite number.
70
+ * @throws Error when the value is not a positive number
71
+ */
72
+ export declare function parseScale(value: number | string): number;
73
+ /**
74
+ * Configuration baked into the generated runtime script at YAML-conversion
75
+ * time. Everything else (screens, references) is read at runtime from
76
+ * .ci/inputs/params.json.
77
+ */
78
+ export interface RenderScriptConfig {
79
+ packagePath: string;
80
+ target: string;
81
+ width: number;
82
+ height: number;
83
+ scale: number;
84
+ }
85
+ /** Options consumed by the pure Swift-source generators. */
86
+ export interface HarnessGeneratorOptions {
87
+ packagePath: string;
88
+ packageRef: string;
89
+ target: string;
90
+ width: number;
91
+ height: number;
92
+ scale: number;
93
+ }
94
+ /** The generator functions embedded in (and shared with) the runtime script. */
95
+ export interface RenderScriptInternals {
96
+ swiftStringLiteral(value: string): string;
97
+ generateHarnessPackageSwift(screens: string[], options: HarnessGeneratorOptions): string;
98
+ generateProbeSwift(options: HarnessGeneratorOptions): string;
99
+ generateScreenSwift(screen: string, options: HarnessGeneratorOptions): string;
100
+ }
101
+ /**
102
+ * Evaluates the embedded generator source and returns the real functions, so
103
+ * unit tests exercise exactly the code that ships inside the runtime script.
104
+ */
105
+ export declare function getRenderScriptInternals(): RenderScriptInternals;
106
+ /**
107
+ * Generates the self-contained runtime node script for the step.
108
+ * Pure function of its config — exported so tests can execute the script.
109
+ */
110
+ export declare function generateRenderScript(config: RenderScriptConfig): string;
111
+ /**
112
+ * ui-fidelity-render step executor.
113
+ * Returns a node-kind StepDef whose script performs the render at runtime on
114
+ * the macOS runner.
115
+ */
116
+ export declare class UiFidelityRenderStepExecutor extends BaseStepExecutor {
117
+ getValidationRequirements(inputs: UiFidelityRenderInputs, _env: Record<string, string>, _config: CIConfig): ValidationRequirement[];
118
+ execute(inputs: UiFidelityRenderInputs, _env: Record<string, string>, _config: CIConfig): Promise<StepDef>;
119
+ }
120
+ //# sourceMappingURL=ui-fidelity-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;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;AAEpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACzF,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;CAC/E;AA6eD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,qBAAqB,CAShE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAQvE;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAoBpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAoBpB"}