@invarn/cibuild 1.7.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.
@@ -1,5 +1,48 @@
1
+ export interface SetupOptions {
2
+ keystore: boolean;
3
+ keystoreProperties: boolean;
4
+ googleServices: boolean;
5
+ googlePlayDeploy: boolean;
6
+ googlePlayPackageName: string;
7
+ /** Whether the release workflow should produce an AAB or APK. Defaults to 'apk'. */
8
+ artifactType: 'apk' | 'aab';
9
+ /** Resolved keystore file paths per workflow (relative to project root), if found on disk. */
10
+ keystorePaths: Record<string, string>;
11
+ }
12
+ export interface WorkflowVariantConfig {
13
+ /** Value for the VARIANT env var, e.g. "debug", "release", "stagingDebug". */
14
+ variant: string;
15
+ /** Capitalised build type for gradle-build inputs, e.g. "Debug", "Release", "StagingDebug". */
16
+ buildType: string;
17
+ /** Gradle assemble task, e.g. "assembleDebug", "assembleRelease", "assembleStagingDebug". */
18
+ gradleTask: string;
19
+ }
20
+ export interface WorkflowVariants {
21
+ primary: WorkflowVariantConfig;
22
+ pullRequest: WorkflowVariantConfig;
23
+ release: WorkflowVariantConfig;
24
+ }
25
+ export interface IosWorkflowSchemeConfig {
26
+ scheme: string;
27
+ configuration: string;
28
+ distributionMethod: string;
29
+ }
30
+ export interface IosWorkflowVariants {
31
+ primary: IosWorkflowSchemeConfig;
32
+ pullRequest: IosWorkflowSchemeConfig;
33
+ release: IosWorkflowSchemeConfig;
34
+ }
35
+ export interface IosSetupOptions {
36
+ cocoaPods: boolean;
37
+ codeSigning: boolean;
38
+ appStoreDeploy: boolean;
39
+ bundleId: string;
40
+ }
41
+ export declare function generateAndroidPipeline(javaVersion?: number, setup?: SetupOptions, variants?: WorkflowVariants, cacheTechnology?: "gradle" | "kmm", metaNamespace?: 'invarn' | 'cibuild.io'): string;
42
+ export declare function generateIosPipeline(projectPath: string, setup: IosSetupOptions, variants: IosWorkflowVariants, metaNamespace?: 'invarn' | 'cibuild.io'): string;
1
43
  export declare function handleBuildCommand(detectMobileProjectRoot: (dir: string) => "android" | "ios" | "kmm" | null, options?: {
2
44
  createPipelinesDir?: boolean;
3
45
  nonInteractive?: boolean;
46
+ metaNamespace?: 'invarn' | 'cibuild.io';
4
47
  }): Promise<void>;
5
48
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAm8BA,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,EAC1E,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACvE,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oFAAoF;IACpF,YAAY,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,8FAA8F;IAC9F,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAiBD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,uBAAuB,CAAC;IACjC,WAAW,EAAE,uBAAuB,CAAC;IACrC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoJD,wBAAgB,uBAAuB,CACrC,WAAW,GAAE,MAAW,EACxB,KAAK,GAAE,YAAgL,EACvL,QAAQ,GAAE,gBAAmC,EAC7C,eAAe,GAAE,QAAQ,GAAG,KAAgB,EAC5C,aAAa,GAAE,QAAQ,GAAG,YAA2B,GACpD,MAAM,CA2NR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,GAAE,QAAQ,GAAG,YAA2B,GACpD,MAAM,CA4LR;AAgVD,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,EAC1E,OAAO,GAAE;IACP,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACpC,GACL,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -122,7 +122,7 @@ function detectSetupNeeds(warnings) {
122
122
  artifactType: 'apk',
123
123
  };
124
124
  }
125
- function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle") {
125
+ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle", metaNamespace = 'cibuild.io') {
126
126
  const keystoreStepFor = (wf) => {
127
127
  if (!setup.keystore)
128
128
  return "";
@@ -204,7 +204,7 @@ function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, ke
204
204
  return `format_version: '1'
205
205
 
206
206
  meta:
207
- cibuild.io:
207
+ ${metaNamespace}:
208
208
  stack: ${stack}
209
209
  machine_type: standard
210
210
  platform: ${platformMeta}
@@ -333,7 +333,7 @@ ${googlePlayStep}
333
333
  fi
334
334
  `;
335
335
  }
336
- function generateIosPipeline(projectPath, setup, variants) {
336
+ export function generateIosPipeline(projectPath, setup, variants, metaNamespace = 'cibuild.io') {
337
337
  const { primary: pv, pullRequest: prv, release: rv } = variants;
338
338
  // Global scheme is the primary scheme (used as default across workflows)
339
339
  const globalScheme = pv.scheme;
@@ -409,7 +409,7 @@ function generateIosPipeline(projectPath, setup, variants) {
409
409
  return `format_version: '1'
410
410
 
411
411
  meta:
412
- cibuild.io:
412
+ ${metaNamespace}:
413
413
  stack: macos-xcode-26.4
414
414
  machine_type: performance
415
415
  platform: ios
@@ -771,7 +771,7 @@ async function handleIosBuildCommand(cwd, options = {}) {
771
771
  }
772
772
  }
773
773
  // 7. Generate and write the pipeline
774
- const yaml = generateIosPipeline(iosScanResult.projectPath, iosSetupOptions, iosVariants);
774
+ const yaml = generateIosPipeline(iosScanResult.projectPath, iosSetupOptions, iosVariants, options.metaNamespace);
775
775
  writeFileSync(outputPath, yaml, "utf-8");
776
776
  console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
777
777
  console.log(" Platform: iOS");
@@ -1042,7 +1042,7 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
1042
1042
  // that exact JDK — a newer JDK can cross-compile. Enforce a minimum of 17
1043
1043
  // since ubuntu-latest runners ship with JDK 17+ and JDK <17 is unavailable.
1044
1044
  const javaVersion = Math.max(scanResult.detectedJavaVersion ?? 17, 17);
1045
- const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants, cacheTechnology);
1045
+ const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants, cacheTechnology, options.metaNamespace);
1046
1046
  writeFileSync(outputPath, yaml, "utf-8");
1047
1047
  console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
1048
1048
  console.log(` Platform: ${platformLabel}`);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Unit tests for the YAML scaffold generators in build.ts.
3
+ *
4
+ * `generateIosPipeline` and `generateAndroidPipeline` are pure
5
+ * functions today (input → YAML string), so we drive them directly
6
+ * rather than through the interactive `handleBuildCommand` shell.
7
+ *
8
+ * Focus is the meta-namespace flip: standalone `cibuild` callers get
9
+ * `meta.cibuild.io:` (default), while invarn-CLI-routed calls pass
10
+ * `metaNamespace: 'invarn'` and get `meta.invarn:`.
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.test.d.ts","sourceRoot":"","sources":["../../../src/commands/build.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Unit tests for the YAML scaffold generators in build.ts.
3
+ *
4
+ * `generateIosPipeline` and `generateAndroidPipeline` are pure
5
+ * functions today (input → YAML string), so we drive them directly
6
+ * rather than through the interactive `handleBuildCommand` shell.
7
+ *
8
+ * Focus is the meta-namespace flip: standalone `cibuild` callers get
9
+ * `meta.cibuild.io:` (default), while invarn-CLI-routed calls pass
10
+ * `metaNamespace: 'invarn'` and get `meta.invarn:`.
11
+ */
12
+ import { describe, test, expect } from '@jest/globals';
13
+ import { generateIosPipeline, generateAndroidPipeline, } from './build.js';
14
+ const iosSetup = {
15
+ cocoaPods: false,
16
+ codeSigning: false,
17
+ appStoreDeploy: false,
18
+ bundleId: 'com.example.app',
19
+ };
20
+ const iosVariants = {
21
+ primary: { scheme: 'MyApp', configuration: 'Debug', distributionMethod: 'development' },
22
+ pullRequest: { scheme: 'MyApp', configuration: 'Debug', distributionMethod: 'development' },
23
+ release: { scheme: 'MyApp', configuration: 'Release', distributionMethod: 'app-store' },
24
+ };
25
+ describe('generateIosPipeline — meta namespace', () => {
26
+ test('emits meta.cibuild.io: by default (standalone cibuild path)', () => {
27
+ const yaml = generateIosPipeline('./MyApp', iosSetup, iosVariants);
28
+ expect(yaml).toContain('meta:\n cibuild.io:\n');
29
+ expect(yaml).not.toContain('meta:\n invarn:\n');
30
+ });
31
+ test('emits meta.cibuild.io: when explicitly passed "cibuild.io"', () => {
32
+ const yaml = generateIosPipeline('./MyApp', iosSetup, iosVariants, 'cibuild.io');
33
+ expect(yaml).toContain('meta:\n cibuild.io:\n');
34
+ expect(yaml).not.toContain('meta:\n invarn:\n');
35
+ });
36
+ test('emits meta.invarn: when explicitly passed "invarn" (invarn-CLI path)', () => {
37
+ const yaml = generateIosPipeline('./MyApp', iosSetup, iosVariants, 'invarn');
38
+ expect(yaml).toContain('meta:\n invarn:\n');
39
+ expect(yaml).not.toContain('meta:\n cibuild.io:\n');
40
+ });
41
+ });
42
+ describe('generateAndroidPipeline — meta namespace', () => {
43
+ test('emits meta.cibuild.io: by default (standalone cibuild path)', () => {
44
+ const yaml = generateAndroidPipeline();
45
+ expect(yaml).toContain('meta:\n cibuild.io:\n');
46
+ expect(yaml).not.toContain('meta:\n invarn:\n');
47
+ });
48
+ test('emits meta.cibuild.io: when explicitly passed "cibuild.io"', () => {
49
+ const yaml = generateAndroidPipeline(17, undefined, undefined, 'gradle', 'cibuild.io');
50
+ expect(yaml).toContain('meta:\n cibuild.io:\n');
51
+ expect(yaml).not.toContain('meta:\n invarn:\n');
52
+ });
53
+ test('emits meta.invarn: when explicitly passed "invarn" (invarn-CLI path)', () => {
54
+ const yaml = generateAndroidPipeline(17, undefined, undefined, 'gradle', 'invarn');
55
+ expect(yaml).toContain('meta:\n invarn:\n');
56
+ expect(yaml).not.toContain('meta:\n cibuild.io:\n');
57
+ });
58
+ });
59
+ //# sourceMappingURL=build.test.js.map
@@ -1,6 +1,13 @@
1
1
  export interface InitOptions {
2
2
  importPath?: string;
3
3
  create?: boolean;
4
+ /**
5
+ * Which namespace to emit in the generated `meta.<ns>` block of the new
6
+ * pipeline YAML. Defaults to `'cibuild.io'` so the standalone `cibuild`
7
+ * CLI keeps emitting the legacy namespace. Invarn-CLI-routed calls pass
8
+ * `'invarn'` so wrapped flows emit the canonical Invarn namespace.
9
+ */
10
+ metaNamespace?: 'invarn' | 'cibuild.io';
4
11
  }
5
12
  /**
6
13
  * Check dependencies, then either import an existing YAML pipeline or
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAUD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0Q7E"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACzC;AAUD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2Q7E"}
@@ -189,6 +189,7 @@ export async function handleInitCommand(opts = {}) {
189
189
  await handleBuildCommand(detectMobileProjectRoot, {
190
190
  createPipelinesDir: true,
191
191
  nonInteractive: !!opts.create,
192
+ metaNamespace: opts.metaNamespace,
192
193
  });
193
194
  process.exit(0);
194
195
  }
@@ -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;
@@ -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;AAKjE,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,GAAG,OAAO,CAAC,IAAI,CAAC;IAsI3C,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;CA0CpG"}
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"}
@@ -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 (const stepId of pipeline.stepsOrder) {
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