@invarn/cibuild 1.8.0 → 1.9.0
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 +1 -1
- package/dist/src/runner.d.ts +1 -1
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +19 -3
- package/dist/src/step-marker.d.ts +12 -0
- package/dist/src/step-marker.d.ts.map +1 -0
- package/dist/src/step-marker.js +29 -0
- package/dist/src/step-marker.test.d.ts +11 -0
- package/dist/src/step-marker.test.d.ts.map +1 -0
- package/dist/src/step-marker.test.js +45 -0
- package/dist/src/yaml/steps/xcode-destination.d.ts +47 -0
- package/dist/src/yaml/steps/xcode-destination.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-destination.js +202 -0
- package/dist/src/yaml/steps/xcode-destination.test.d.ts +9 -0
- package/dist/src/yaml/steps/xcode-destination.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-destination.test.js +227 -0
- package/dist/src/yaml/steps/xcode.d.ts +1 -1
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +31 -9
- package/package.json +1 -1
package/dist/src/runner.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare class StepExecutionError extends Error {
|
|
|
8
8
|
export declare class PipelineRunner {
|
|
9
9
|
private config;
|
|
10
10
|
constructor(config: CIConfig);
|
|
11
|
-
runStep(step: StepDef): Promise<void>;
|
|
11
|
+
runStep(step: StepDef, stepNumber?: number): Promise<void>;
|
|
12
12
|
private formatDuration;
|
|
13
13
|
validatePipeline(pipeline: PipelineDef): void;
|
|
14
14
|
displayWarnings(warnings: string[], skippedSteps: number): void;
|
package/dist/src/runner.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/src/runner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { unlinkSync, existsSync, writeFileSync, chmodSync, mkdtempSync, rmSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import { formatStepMarker, shouldEmitStepMarker } from "./step-marker.js";
|
|
5
6
|
// Use absolute path for envstore so it works regardless of cd in scripts
|
|
6
7
|
const ENVMAN_ENVSTORE_PATH = resolve(process.cwd(), ".ci", ".envstore.json");
|
|
7
8
|
export class StepExecutionError extends Error {
|
|
@@ -21,7 +22,21 @@ export class PipelineRunner {
|
|
|
21
22
|
constructor(config) {
|
|
22
23
|
this.config = config;
|
|
23
24
|
}
|
|
24
|
-
async runStep(step) {
|
|
25
|
+
async runStep(step, stepNumber) {
|
|
26
|
+
// Emit the INVARN_STEP marker before any other output so the
|
|
27
|
+
// dashboard's step-progress parser sees a step boundary even when
|
|
28
|
+
// the step's own logs are silent. Pairs with the parser at
|
|
29
|
+
// `lib/shared/step-progress.ts` in the Invarn repo. `stepNumber`
|
|
30
|
+
// is optional to keep `runStep` backward-compatible for direct
|
|
31
|
+
// callers (lib consumers) that don't run inside a pipeline loop.
|
|
32
|
+
//
|
|
33
|
+
// Gated on INVARN_RUNNER=1: cibuild is a public npm package
|
|
34
|
+
// (@invarn/cibuild) and must not leak Invarn's log-parsing protocol
|
|
35
|
+
// into external users' stdout. The Invarn runner opts in by exporting
|
|
36
|
+
// the var into the sandbox; see cibuild-runner/src/runner.ts.
|
|
37
|
+
if (stepNumber != null && shouldEmitStepMarker()) {
|
|
38
|
+
console.log(formatStepMarker(stepNumber, step.name));
|
|
39
|
+
}
|
|
25
40
|
console.log(`\n[${step.id}] Running: ${step.name}`);
|
|
26
41
|
console.log("─".repeat(60));
|
|
27
42
|
const startTime = Date.now();
|
|
@@ -209,13 +224,14 @@ export -f envman
|
|
|
209
224
|
}
|
|
210
225
|
this.validatePipeline(pipeline);
|
|
211
226
|
const stepsMap = new Map(pipeline.steps.map((s) => [s.id, s]));
|
|
212
|
-
for (
|
|
227
|
+
for (let i = 0; i < pipeline.stepsOrder.length; i++) {
|
|
228
|
+
const stepId = pipeline.stepsOrder[i];
|
|
213
229
|
const step = stepsMap.get(stepId);
|
|
214
230
|
if (!step) {
|
|
215
231
|
throw new Error(`Step ${stepId} not found`);
|
|
216
232
|
}
|
|
217
233
|
try {
|
|
218
|
-
await this.runStep(step);
|
|
234
|
+
await this.runStep(step, i + 1);
|
|
219
235
|
}
|
|
220
236
|
catch (err) {
|
|
221
237
|
if (step.isSkippable && err instanceof StepExecutionError) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function formatStepMarker(stepNumber: number, stepName: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* cibuild ships publicly on npm as `@invarn/cibuild`. Emitting the marker
|
|
4
|
+
* unconditionally would leak Invarn's log-parsing protocol into external
|
|
5
|
+
* users' stdout. The Invarn runner (cibuild-runner) opts in by exporting
|
|
6
|
+
* INVARN_RUNNER=1 into the sandbox; everywhere else this returns false
|
|
7
|
+
* and the runner stays quiet.
|
|
8
|
+
*
|
|
9
|
+
* Strict equality to "1" — no truthy parsing — keeps the contract crisp.
|
|
10
|
+
*/
|
|
11
|
+
export declare function shouldEmitStepMarker(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
//# sourceMappingURL=step-marker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-marker.d.ts","sourceRoot":"","sources":["../../src/step-marker.ts"],"names":[],"mappings":"AAcA,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAElF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer side of the INVARN_STEP marker contract.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard's `lib/shared/step-progress.ts` parses runtime logs for
|
|
5
|
+
* lines matching `/▸ INVARN_STEP:(\d+):(.+)/` to populate
|
|
6
|
+
* `BuildEnrichment.parsedSteps`. The runner emits one marker per step
|
|
7
|
+
* via `console.log(formatStepMarker(...))` before executing it, so every
|
|
8
|
+
* build (agent or not) gets symmetric step telemetry.
|
|
9
|
+
*
|
|
10
|
+
* Step name is embedded as-is. The dashboard parser uses `(.+)` (greedy),
|
|
11
|
+
* so step names containing colons (`build:android:debug`) round-trip.
|
|
12
|
+
*/
|
|
13
|
+
const STEP_MARKER_PREFIX = '▸ INVARN_STEP';
|
|
14
|
+
export function formatStepMarker(stepNumber, stepName) {
|
|
15
|
+
return `${STEP_MARKER_PREFIX}:${stepNumber}:${stepName}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* cibuild ships publicly on npm as `@invarn/cibuild`. Emitting the marker
|
|
19
|
+
* unconditionally would leak Invarn's log-parsing protocol into external
|
|
20
|
+
* users' stdout. The Invarn runner (cibuild-runner) opts in by exporting
|
|
21
|
+
* INVARN_RUNNER=1 into the sandbox; everywhere else this returns false
|
|
22
|
+
* and the runner stays quiet.
|
|
23
|
+
*
|
|
24
|
+
* Strict equality to "1" — no truthy parsing — keeps the contract crisp.
|
|
25
|
+
*/
|
|
26
|
+
export function shouldEmitStepMarker(env = process.env) {
|
|
27
|
+
return env.INVARN_RUNNER === '1';
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=step-marker.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the INVARN_STEP marker formatter.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard's `lib/shared/step-progress.ts` parses runtime logs for
|
|
5
|
+
* lines matching `/▸ INVARN_STEP:(\d+):(.+)/` to populate
|
|
6
|
+
* `BuildEnrichment.parsedSteps`. This formatter is the producer side of
|
|
7
|
+
* that contract — the runner emits one marker per step before executing
|
|
8
|
+
* it so non-agent builds get symmetric step telemetry.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=step-marker.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-marker.test.d.ts","sourceRoot":"","sources":["../../src/step-marker.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the INVARN_STEP marker formatter.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard's `lib/shared/step-progress.ts` parses runtime logs for
|
|
5
|
+
* lines matching `/▸ INVARN_STEP:(\d+):(.+)/` to populate
|
|
6
|
+
* `BuildEnrichment.parsedSteps`. This formatter is the producer side of
|
|
7
|
+
* that contract — the runner emits one marker per step before executing
|
|
8
|
+
* it so non-agent builds get symmetric step telemetry.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from '@jest/globals';
|
|
11
|
+
import { formatStepMarker, shouldEmitStepMarker } from './step-marker.js';
|
|
12
|
+
describe('formatStepMarker', () => {
|
|
13
|
+
test('formats a step number and name as the wire-format marker', () => {
|
|
14
|
+
expect(formatStepMarker(1, 'install-deps')).toBe('▸ INVARN_STEP:1:install-deps');
|
|
15
|
+
});
|
|
16
|
+
test('preserves colons in the step name (the dashboard parser is greedy on the trailing capture)', () => {
|
|
17
|
+
expect(formatStepMarker(2, 'build:android:debug')).toBe('▸ INVARN_STEP:2:build:android:debug');
|
|
18
|
+
});
|
|
19
|
+
test('uses 1-indexed step numbers (matches the agent generator and dashboard parser)', () => {
|
|
20
|
+
expect(formatStepMarker(3, 'lint')).toBe('▸ INVARN_STEP:3:lint');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Gate that keeps the marker off when cibuild is used outside Invarn
|
|
25
|
+
* infrastructure. cibuild ships publicly on npm as @invarn/cibuild and
|
|
26
|
+
* must not leak Invarn's log-parsing protocol into external users' stdout.
|
|
27
|
+
* The Invarn runner (cibuild-runner) opts in by exporting INVARN_RUNNER=1
|
|
28
|
+
* into the sandbox alongside CIBUILD_NON_INTERACTIVE / CI.
|
|
29
|
+
*/
|
|
30
|
+
describe('shouldEmitStepMarker', () => {
|
|
31
|
+
test('returns true only when INVARN_RUNNER === "1"', () => {
|
|
32
|
+
expect(shouldEmitStepMarker({ INVARN_RUNNER: '1' })).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
test('returns false when INVARN_RUNNER is absent (public/external cibuild use)', () => {
|
|
35
|
+
expect(shouldEmitStepMarker({})).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
test('returns false for INVARN_RUNNER values that are not literally "1"', () => {
|
|
38
|
+
// Strict equality keeps the wire-level handshake unambiguous — no truthy parsing.
|
|
39
|
+
expect(shouldEmitStepMarker({ INVARN_RUNNER: '0' })).toBe(false);
|
|
40
|
+
expect(shouldEmitStepMarker({ INVARN_RUNNER: 'true' })).toBe(false);
|
|
41
|
+
expect(shouldEmitStepMarker({ INVARN_RUNNER: '' })).toBe(false);
|
|
42
|
+
expect(shouldEmitStepMarker({ INVARN_RUNNER: ' 1 ' })).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=step-marker.test.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves `destination: "auto"` for xcode-* step executors.
|
|
3
|
+
*
|
|
4
|
+
* `auto` queries `xcrun simctl list devices available --json` and picks the
|
|
5
|
+
* newest iPhone in the highest installed iOS runtime, returning a canonical
|
|
6
|
+
* destination string for xcodebuild: `platform=iOS Simulator,OS=<v>,name=<n>`.
|
|
7
|
+
*
|
|
8
|
+
* The host shell-out lives in {@link resolveAutoDestination} so the parser
|
|
9
|
+
* ({@link resolveAutoDestinationFromSimctlJson}) stays pure and unit-testable
|
|
10
|
+
* against fixture JSON.
|
|
11
|
+
*/
|
|
12
|
+
/** Failure mode for "auto" resolution. Surfaces as the step's failure reason. */
|
|
13
|
+
export declare class AutoDestinationError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pure helper — parses `xcrun simctl list devices available --json` output
|
|
18
|
+
* and returns the canonical destination string.
|
|
19
|
+
*
|
|
20
|
+
* Selection rules (per spike spec):
|
|
21
|
+
* - Highest available iOS runtime: highest semver among keys matching
|
|
22
|
+
* `com.apple.CoreSimulator.SimRuntime.iOS-*`.
|
|
23
|
+
* - Newest iPhone: device whose name starts with "iPhone" and has the highest
|
|
24
|
+
* model number suffix (e.g. "iPhone 16 Pro Max" > "iPhone 15" >
|
|
25
|
+
* "iPhone SE (3rd generation)"). Ties at the highest model number prefer
|
|
26
|
+
* "Pro Max", then "Pro", then plain.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveAutoDestinationFromSimctlJson(rawJson: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Pass-through resolver used by xcode-* executors.
|
|
31
|
+
*
|
|
32
|
+
* Returns the literal value unchanged when it isn't the magic string `"auto"`.
|
|
33
|
+
* When it is, delegates to a resolver (default: {@link resolveAutoDestination},
|
|
34
|
+
* which shells out to `xcrun simctl`). The resolver injection point exists for
|
|
35
|
+
* tests, so executor-level tests can verify the "auto" branch without
|
|
36
|
+
* touching the host's xcrun.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveDestinationInput(value: string, autoResolver?: () => string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Shell-out wrapper: runs `xcrun simctl list devices available --json` and
|
|
41
|
+
* delegates the parse to {@link resolveAutoDestinationFromSimctlJson}.
|
|
42
|
+
*
|
|
43
|
+
* Surfaces the documented failure modes (xcrun missing, simctl failure,
|
|
44
|
+
* no iPhone runtime) as {@link AutoDestinationError}.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveAutoDestination(): string;
|
|
47
|
+
//# sourceMappingURL=xcode-destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xcode-destination.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-destination.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,iFAAiF;AACjF,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA4B5E;AAiID;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,YAAY,GAAE,MAAM,MAA+B,GAClD,MAAM,CAGR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves `destination: "auto"` for xcode-* step executors.
|
|
3
|
+
*
|
|
4
|
+
* `auto` queries `xcrun simctl list devices available --json` and picks the
|
|
5
|
+
* newest iPhone in the highest installed iOS runtime, returning a canonical
|
|
6
|
+
* destination string for xcodebuild: `platform=iOS Simulator,OS=<v>,name=<n>`.
|
|
7
|
+
*
|
|
8
|
+
* The host shell-out lives in {@link resolveAutoDestination} so the parser
|
|
9
|
+
* ({@link resolveAutoDestinationFromSimctlJson}) stays pure and unit-testable
|
|
10
|
+
* against fixture JSON.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
/** Failure mode for "auto" resolution. Surfaces as the step's failure reason. */
|
|
14
|
+
export class AutoDestinationError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'AutoDestinationError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pure helper — parses `xcrun simctl list devices available --json` output
|
|
22
|
+
* and returns the canonical destination string.
|
|
23
|
+
*
|
|
24
|
+
* Selection rules (per spike spec):
|
|
25
|
+
* - Highest available iOS runtime: highest semver among keys matching
|
|
26
|
+
* `com.apple.CoreSimulator.SimRuntime.iOS-*`.
|
|
27
|
+
* - Newest iPhone: device whose name starts with "iPhone" and has the highest
|
|
28
|
+
* model number suffix (e.g. "iPhone 16 Pro Max" > "iPhone 15" >
|
|
29
|
+
* "iPhone SE (3rd generation)"). Ties at the highest model number prefer
|
|
30
|
+
* "Pro Max", then "Pro", then plain.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveAutoDestinationFromSimctlJson(rawJson) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(rawJson);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new AutoDestinationError('Failed to resolve destination: auto — xcrun simctl returned no usable output. ' +
|
|
39
|
+
'Pin a specific destination string instead.');
|
|
40
|
+
}
|
|
41
|
+
const iosRuntime = pickHighestIosRuntime(parsed.devices ?? {});
|
|
42
|
+
if (!iosRuntime) {
|
|
43
|
+
throw new AutoDestinationError('Failed to resolve destination: auto — no iPhone simulator runtimes are installed. ' +
|
|
44
|
+
'Install one via Xcode → Settings → Platforms.');
|
|
45
|
+
}
|
|
46
|
+
const iphone = pickNewestIphone(iosRuntime.devices);
|
|
47
|
+
if (!iphone) {
|
|
48
|
+
throw new AutoDestinationError('Failed to resolve destination: auto — no iPhone simulator runtimes are installed. ' +
|
|
49
|
+
'Install one via Xcode → Settings → Platforms.');
|
|
50
|
+
}
|
|
51
|
+
return `platform=iOS Simulator,OS=${iosRuntime.version},name=${iphone.name}`;
|
|
52
|
+
}
|
|
53
|
+
const IOS_RUNTIME_PREFIX = 'com.apple.CoreSimulator.SimRuntime.iOS-';
|
|
54
|
+
/**
|
|
55
|
+
* Picks the runtime with the highest semver among
|
|
56
|
+
* `com.apple.CoreSimulator.SimRuntime.iOS-*` keys whose device list contains
|
|
57
|
+
* at least one available iPhone.
|
|
58
|
+
*/
|
|
59
|
+
function pickHighestIosRuntime(devicesByRuntime) {
|
|
60
|
+
const candidates = [];
|
|
61
|
+
for (const [key, devices] of Object.entries(devicesByRuntime)) {
|
|
62
|
+
if (!key.startsWith(IOS_RUNTIME_PREFIX))
|
|
63
|
+
continue;
|
|
64
|
+
const versionSuffix = key.slice(IOS_RUNTIME_PREFIX.length); // e.g. "18-0"
|
|
65
|
+
const tuple = parseVersionTuple(versionSuffix.replace(/-/g, '.'));
|
|
66
|
+
if (tuple.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
const availableIphones = devices.filter((d) => isAvailableIphone(d));
|
|
69
|
+
if (availableIphones.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
candidates.push({
|
|
72
|
+
versionTuple: tuple,
|
|
73
|
+
version: tuple.join('.'),
|
|
74
|
+
devices: availableIphones,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (candidates.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
candidates.sort((a, b) => compareTuples(b.versionTuple, a.versionTuple));
|
|
80
|
+
const winner = candidates[0];
|
|
81
|
+
return { version: winner.version, devices: winner.devices };
|
|
82
|
+
}
|
|
83
|
+
function isAvailableIphone(device) {
|
|
84
|
+
if (device.isAvailable === false)
|
|
85
|
+
return false;
|
|
86
|
+
return typeof device.name === 'string' && device.name.startsWith('iPhone');
|
|
87
|
+
}
|
|
88
|
+
/** Parses "18.0" or "18.0.1" into [18, 0] / [18, 0, 1]. */
|
|
89
|
+
function parseVersionTuple(raw) {
|
|
90
|
+
const parts = raw.split('.');
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const p of parts) {
|
|
93
|
+
const n = Number.parseInt(p, 10);
|
|
94
|
+
if (!Number.isFinite(n))
|
|
95
|
+
return [];
|
|
96
|
+
out.push(n);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function compareTuples(a, b) {
|
|
101
|
+
const len = Math.max(a.length, b.length);
|
|
102
|
+
for (let i = 0; i < len; i++) {
|
|
103
|
+
const av = a[i] ?? 0;
|
|
104
|
+
const bv = b[i] ?? 0;
|
|
105
|
+
if (av !== bv)
|
|
106
|
+
return av - bv;
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const VARIANT_RANK = [
|
|
111
|
+
{ pattern: /\bpro\s+max\b/i, rank: 3 },
|
|
112
|
+
{ pattern: /\bpro\b/i, rank: 2 },
|
|
113
|
+
{ pattern: /\bplus\b/i, rank: 1 },
|
|
114
|
+
];
|
|
115
|
+
/**
|
|
116
|
+
* Picks the newest iPhone by model-number suffix, with Pro Max > Pro > plain
|
|
117
|
+
* tie-breaking. iPhones with no numeric model (e.g. "iPhone SE (3rd generation)")
|
|
118
|
+
* are ranked below numeric models.
|
|
119
|
+
*/
|
|
120
|
+
function pickNewestIphone(devices) {
|
|
121
|
+
const scored = [];
|
|
122
|
+
for (const device of devices) {
|
|
123
|
+
if (!isAvailableIphone(device))
|
|
124
|
+
continue;
|
|
125
|
+
scored.push({
|
|
126
|
+
device,
|
|
127
|
+
model: extractIphoneModelNumber(device.name),
|
|
128
|
+
variantRank: extractVariantRank(device.name),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (scored.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
scored.sort((a, b) => {
|
|
134
|
+
if (a.model !== b.model)
|
|
135
|
+
return b.model - a.model;
|
|
136
|
+
if (a.variantRank !== b.variantRank)
|
|
137
|
+
return b.variantRank - a.variantRank;
|
|
138
|
+
return a.device.name.localeCompare(b.device.name);
|
|
139
|
+
});
|
|
140
|
+
return scored[0].device;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* "iPhone 16 Pro Max" → 16. "iPhone 15" → 15. "iPhone SE (3rd generation)" → -1.
|
|
144
|
+
* We look at the token immediately after "iPhone".
|
|
145
|
+
*/
|
|
146
|
+
function extractIphoneModelNumber(name) {
|
|
147
|
+
const match = name.match(/^iPhone\s+(\d+)/i);
|
|
148
|
+
if (!match)
|
|
149
|
+
return -1;
|
|
150
|
+
return Number.parseInt(match[1], 10);
|
|
151
|
+
}
|
|
152
|
+
function extractVariantRank(name) {
|
|
153
|
+
for (const { pattern, rank } of VARIANT_RANK) {
|
|
154
|
+
if (pattern.test(name))
|
|
155
|
+
return rank;
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Pass-through resolver used by xcode-* executors.
|
|
161
|
+
*
|
|
162
|
+
* Returns the literal value unchanged when it isn't the magic string `"auto"`.
|
|
163
|
+
* When it is, delegates to a resolver (default: {@link resolveAutoDestination},
|
|
164
|
+
* which shells out to `xcrun simctl`). The resolver injection point exists for
|
|
165
|
+
* tests, so executor-level tests can verify the "auto" branch without
|
|
166
|
+
* touching the host's xcrun.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveDestinationInput(value, autoResolver = resolveAutoDestination) {
|
|
169
|
+
if (value !== 'auto')
|
|
170
|
+
return value;
|
|
171
|
+
return autoResolver();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Shell-out wrapper: runs `xcrun simctl list devices available --json` and
|
|
175
|
+
* delegates the parse to {@link resolveAutoDestinationFromSimctlJson}.
|
|
176
|
+
*
|
|
177
|
+
* Surfaces the documented failure modes (xcrun missing, simctl failure,
|
|
178
|
+
* no iPhone runtime) as {@link AutoDestinationError}.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveAutoDestination() {
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], {
|
|
184
|
+
encoding: 'utf-8',
|
|
185
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err && (err.code === 'ENOENT' || /not found/i.test(err.message ?? ''))) {
|
|
190
|
+
throw new AutoDestinationError('xcrun not available on this runner; cannot resolve destination: auto. ' +
|
|
191
|
+
'Pin a specific destination string instead.');
|
|
192
|
+
}
|
|
193
|
+
throw new AutoDestinationError('Failed to resolve destination: auto — xcrun simctl returned no usable output. ' +
|
|
194
|
+
'Pin a specific destination string instead.');
|
|
195
|
+
}
|
|
196
|
+
if (!raw || raw.trim() === '') {
|
|
197
|
+
throw new AutoDestinationError('Failed to resolve destination: auto — xcrun simctl returned no usable output. ' +
|
|
198
|
+
'Pin a specific destination string instead.');
|
|
199
|
+
}
|
|
200
|
+
return resolveAutoDestinationFromSimctlJson(raw);
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=xcode-destination.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shared "auto destination" resolver.
|
|
3
|
+
*
|
|
4
|
+
* The resolver consumes the JSON `xcrun simctl list devices available --json`
|
|
5
|
+
* would produce on a macOS runner, and returns the canonical
|
|
6
|
+
* `platform=iOS Simulator,OS=<v>,name=<name>` string to feed to xcodebuild.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=xcode-destination.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xcode-destination.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-destination.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|