@invarn/cibuild 1.3.19 → 1.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/src/cli.js CHANGED
@@ -180,7 +180,7 @@ async function handleInitCommand(opts = {}) {
180
180
  ];
181
181
  const dependencies = [
182
182
  ...commonDependencies,
183
- ...(projectType === "android" ? androidDependencies : iosDependencies),
183
+ ...(projectType === "android" ? androidDependencies : projectType === "kmm" ? [...androidDependencies, ...iosDependencies] : iosDependencies),
184
184
  ];
185
185
  let allRequired = true;
186
186
  let missingOptional = false;
@@ -318,8 +318,8 @@ async function handleInitCommand(opts = {}) {
318
318
  try {
319
319
  const yamlPipeline = loadYAMLPipeline(destPath);
320
320
  const meta = yamlPipeline.meta?.["cibuild.io"];
321
- if (meta?.platform === "ios" || meta?.platform === "android") {
322
- importedPlatform = meta.platform;
321
+ if (meta?.platform === "ios" || meta?.platform === "android" || meta?.platform === "kmm") {
322
+ importedPlatform = meta.platform === "kmm" ? "ios" : meta.platform;
323
323
  }
324
324
  }
325
325
  catch { /* ignore parse errors — default to macos-latest */ }
@@ -455,6 +455,7 @@ Usage:
455
455
  ci run <path> [-w <name>] --skip-validation Skip validation, run with interactive prompts
456
456
  ci validate <path> [-w <name>] Validate pipeline (alias for --validate-only)
457
457
  ci detect-platform <path> [-w <name>] Detect platform from YAML pipeline
458
+ ci detect-project Detect project type (android, ios, kmm)
458
459
  ci reset [--force] Remove all cibuild files and folders
459
460
  ci edit <path> [-w <name>] View pipeline and edit step inputs
460
461
  ci secrets add <var_name> <path> [-w <name>] Add a secret (prompted interactively)
@@ -502,6 +503,12 @@ Examples:
502
503
  await import("./envman/cli.js");
503
504
  return;
504
505
  }
506
+ // Handle detect-project command
507
+ if (args[0] === "detect-project") {
508
+ const projectType = detectMobileProjectRoot(process.cwd());
509
+ console.log(projectType ?? "unknown");
510
+ process.exit(projectType ? 0 : 1);
511
+ }
505
512
  // Handle init command
506
513
  if (args[0] === "init") {
507
514
  const importIdx = args.indexOf("--import");
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AA+7BA,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":"AAk8BA,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"}
@@ -199,13 +199,15 @@ function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, ke
199
199
  const releaseGradleTask = isAab ? `bundle${rv.buildType}` : rv.gradleTask;
200
200
  const releaseArtifactLabel = isAab ? 'AAB' : 'APK';
201
201
  const releaseArtifactExt = isAab ? '*.aab' : '*.apk';
202
+ const stack = cacheTechnology === "kmm" ? "macos-xcode-26.4" : "linux-docker-android-22.04";
203
+ const platformMeta = cacheTechnology === "kmm" ? "kmm" : "android";
202
204
  return `format_version: '1'
203
205
 
204
206
  meta:
205
207
  cibuild.io:
206
- stack: linux-docker-android-22.04
208
+ stack: ${stack}
207
209
  machine_type: standard
208
- platform: android
210
+ platform: ${platformMeta}
209
211
 
210
212
  app:
211
213
  envs:
@@ -293,8 +293,8 @@ async function handleAddRegisteredStep(pipelinePath, workflowName, _pipeline, st
293
293
  function detectWorkflowPlatform(pipeline, workflowName) {
294
294
  // 1. Explicit platform declaration in meta
295
295
  const metaPlatform = pipeline.meta?.['cibuild.io']?.platform;
296
- if (metaPlatform === 'ios' || metaPlatform === 'android')
297
- return metaPlatform;
296
+ if (metaPlatform === 'ios' || metaPlatform === 'android' || metaPlatform === 'kmm')
297
+ return metaPlatform === 'kmm' ? 'ios' : metaPlatform;
298
298
  // 2. Scan existing steps as fallback
299
299
  const workflow = pipeline.workflows[workflowName];
300
300
  if (!workflow)
@@ -1 +1 @@
1
- {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAKzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAkCD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAgH3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
1
+ {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAKzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAkCD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CA0I3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
@@ -60,11 +60,35 @@ export async function convertYAMLToPipelineDef(pipeline, config, workflowName, y
60
60
  const platformInfo = detectPlatformInfo(pipeline, selectedWorkflowName);
61
61
  // Step 3: Environment variable resolution
62
62
  const envResolver = new EnvResolver(pipeline, selectedWorkflow, selectedWorkflowName, platformInfo.platform, platformInfo.stack, yamlFilePath);
63
+ // Step 3.5: Auto-inject cache steps if cachedBuild is enabled
64
+ const cibuildMeta = pipeline.meta?.['cibuild.io'];
65
+ const cachedBuild = cibuildMeta?.cachedBuild === true;
66
+ let workflowSteps = [...selectedWorkflow.steps];
67
+ if (cachedBuild && cibuildMeta?.platform) {
68
+ const platformToCacheTechnology = {
69
+ ios: 'ios',
70
+ android: 'gradle',
71
+ kmm: 'kmm',
72
+ };
73
+ const technology = platformToCacheTechnology[cibuildMeta.platform];
74
+ if (technology) {
75
+ // Remove any manually written cache-pull/cache-push steps
76
+ workflowSteps = workflowSteps.filter(step => {
77
+ const stepKey = Object.keys(step)[0];
78
+ const stepName = stepKey.split('@')[0];
79
+ return stepName !== 'cache-pull' && stepName !== 'cache-push';
80
+ });
81
+ const cachePullStep = { 'cache-pull@1.0.0': { inputs: { technology } } };
82
+ const cachePushStep = { 'cache-push@1.0.0': { inputs: { technology } } };
83
+ workflowSteps.unshift(cachePullStep);
84
+ workflowSteps.push(cachePushStep);
85
+ }
86
+ }
63
87
  // Step 4: Parse and convert steps
64
88
  const steps = [];
65
89
  const stepsOrder = [];
66
90
  let stepCounter = 0;
67
- for (const yamlStep of selectedWorkflow.steps) {
91
+ for (const yamlStep of workflowSteps) {
68
92
  // Parse step (sub-task 3.8)
69
93
  const parsedStep = parseStep(yamlStep, stepCounter);
70
94
  stepCounter++;
@@ -6,6 +6,7 @@ import { convertYAMLToPipelineDef, YAMLConversionError } from './converter.js';
6
6
  import { clearRegistry, registerStep } from './steps/registry.js';
7
7
  import { ScriptStepExecutor } from './steps/script.js';
8
8
  import { CocoapodsInstallStepExecutor } from './steps/ios-deps.js';
9
+ import { CachePullStepExecutor, CachePushStepExecutor } from './steps/cache.js';
9
10
  describe('convertYAMLToPipelineDef - Warning System', () => {
10
11
  let mockConfig;
11
12
  beforeEach(() => {
@@ -344,5 +345,153 @@ describe('convertYAMLToPipelineDef - Warning System', () => {
344
345
  expect(result.skippedSteps).toBeGreaterThan(0);
345
346
  });
346
347
  });
348
+ describe('cachedBuild auto-injection', () => {
349
+ beforeEach(() => {
350
+ registerStep('cache-pull', new CachePullStepExecutor());
351
+ registerStep('cache-push', new CachePushStepExecutor());
352
+ });
353
+ test('should inject cache-pull and cache-push when cachedBuild is true', async () => {
354
+ const pipeline = {
355
+ format_version: '1',
356
+ meta: {
357
+ 'cibuild.io': {
358
+ stack: 'macos-xcode-26.4',
359
+ platform: 'kmm',
360
+ cachedBuild: true,
361
+ },
362
+ },
363
+ workflows: {
364
+ build: {
365
+ steps: [
366
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
367
+ ],
368
+ },
369
+ },
370
+ };
371
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
372
+ expect(result.pipeline.steps.length).toBe(3);
373
+ expect(result.pipeline.steps[0].name).toBe('cache-pull');
374
+ expect(result.pipeline.steps[0].script).toContain('kmm-');
375
+ expect(result.pipeline.steps[1].name).toBe('script');
376
+ expect(result.pipeline.steps[2].name).toBe('cache-push');
377
+ expect(result.pipeline.steps[2].script).toContain('kmm-');
378
+ });
379
+ test('should not inject cache steps when cachedBuild is false', async () => {
380
+ const pipeline = {
381
+ format_version: '1',
382
+ meta: {
383
+ 'cibuild.io': {
384
+ stack: 'macos-xcode-26.4',
385
+ platform: 'kmm',
386
+ cachedBuild: false,
387
+ },
388
+ },
389
+ workflows: {
390
+ build: {
391
+ steps: [
392
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
393
+ ],
394
+ },
395
+ },
396
+ };
397
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
398
+ expect(result.pipeline.steps.length).toBe(1);
399
+ expect(result.pipeline.steps[0].name).toBe('script');
400
+ });
401
+ test('should not inject cache steps when cachedBuild is absent', async () => {
402
+ const pipeline = {
403
+ format_version: '1',
404
+ meta: {
405
+ 'cibuild.io': {
406
+ stack: 'macos-xcode-26.4',
407
+ platform: 'kmm',
408
+ },
409
+ },
410
+ workflows: {
411
+ build: {
412
+ steps: [
413
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
414
+ ],
415
+ },
416
+ },
417
+ };
418
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
419
+ expect(result.pipeline.steps.length).toBe(1);
420
+ });
421
+ test('should use correct technology for android platform', async () => {
422
+ const pipeline = {
423
+ format_version: '1',
424
+ meta: {
425
+ 'cibuild.io': {
426
+ stack: 'linux-docker-android-22.04',
427
+ platform: 'android',
428
+ cachedBuild: true,
429
+ },
430
+ },
431
+ workflows: {
432
+ build: {
433
+ steps: [
434
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
435
+ ],
436
+ },
437
+ },
438
+ };
439
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
440
+ expect(result.pipeline.steps[0].script).toContain('gradle-');
441
+ expect(result.pipeline.steps[2].script).toContain('gradle-');
442
+ });
443
+ test('should use cocoapods technology for ios platform', async () => {
444
+ const pipeline = {
445
+ format_version: '1',
446
+ meta: {
447
+ 'cibuild.io': {
448
+ stack: 'macos-xcode-26.4',
449
+ platform: 'ios',
450
+ cachedBuild: true,
451
+ },
452
+ },
453
+ workflows: {
454
+ build: {
455
+ steps: [
456
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
457
+ ],
458
+ },
459
+ },
460
+ };
461
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
462
+ expect(result.pipeline.steps[0].script).toContain('pods-');
463
+ expect(result.pipeline.steps[2].script).toContain('pods-');
464
+ });
465
+ test('should strip manual cache steps when cachedBuild is true', async () => {
466
+ const pipeline = {
467
+ format_version: '1',
468
+ meta: {
469
+ 'cibuild.io': {
470
+ stack: 'macos-xcode-26.4',
471
+ platform: 'kmm',
472
+ cachedBuild: true,
473
+ },
474
+ },
475
+ workflows: {
476
+ build: {
477
+ steps: [
478
+ { 'cache-pull@1.0.0': { inputs: { technology: 'gradle' } } },
479
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
480
+ { 'cache-push@1.0.0': { inputs: { technology: 'gradle' } } },
481
+ ],
482
+ },
483
+ },
484
+ };
485
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
486
+ // Should have 3 steps: auto-injected pull, script, auto-injected push
487
+ // The manual cache steps should be replaced, not duplicated
488
+ expect(result.pipeline.steps.length).toBe(3);
489
+ expect(result.pipeline.steps[0].name).toBe('cache-pull');
490
+ expect(result.pipeline.steps[0].script).toContain('kmm-'); // auto uses kmm, not gradle
491
+ expect(result.pipeline.steps[1].name).toBe('script');
492
+ expect(result.pipeline.steps[2].name).toBe('cache-push');
493
+ expect(result.pipeline.steps[2].script).toContain('kmm-');
494
+ });
495
+ });
347
496
  });
348
497
  //# sourceMappingURL=converter.test.js.map
@@ -97,7 +97,7 @@ function validateYAMLPipeline(parsed, filePath) {
97
97
  }
98
98
  // Validate platform if specified
99
99
  if (parsed.meta.platform) {
100
- const validPlatforms = ['macos', 'linux', 'windows'];
100
+ const validPlatforms = ['macos', 'linux'];
101
101
  if (!validPlatforms.includes(parsed.meta.platform)) {
102
102
  throw new YAMLValidationError(`Invalid platform '${parsed.meta.platform}' in ${filePath}. Must be one of: ${validPlatforms.join(', ')}`);
103
103
  }
@@ -110,9 +110,9 @@ function validateYAMLPipeline(parsed, filePath) {
110
110
  }
111
111
  // Validate stack pattern if specified
112
112
  if (cibuildMeta.stack && typeof cibuildMeta.stack === 'string') {
113
- const stackPattern = /^(macos|linux|windows)-/;
113
+ const stackPattern = /^(macos|linux)-/;
114
114
  if (cibuildMeta.stack !== 'local' && !stackPattern.test(cibuildMeta.stack)) {
115
- throw new YAMLValidationError(`Invalid stack '${cibuildMeta.stack}' in ${filePath}. Stack must be 'local' or start with 'macos-', 'linux-', or 'windows-'`);
115
+ throw new YAMLValidationError(`Invalid stack '${cibuildMeta.stack}' in ${filePath}. Stack must be 'local' or start with 'macos-' or 'linux-'`);
116
116
  }
117
117
  }
118
118
  // Validate machine_type if specified
@@ -20,7 +20,7 @@ export declare class PlatformDetector {
20
20
  * Priority 2: meta.platform
21
21
  * Priority 3: auto-detect from workflow steps
22
22
  *
23
- * @returns Detected platform ('macos', 'linux', or 'windows')
23
+ * @returns Detected platform ('macos' or 'linux')
24
24
  * @throws PlatformDetectionError if detection fails or mixed platforms detected
25
25
  */
26
26
  getPlatform(): Platform;
@@ -29,7 +29,7 @@ export class PlatformDetector {
29
29
  * Priority 2: meta.platform
30
30
  * Priority 3: auto-detect from workflow steps
31
31
  *
32
- * @returns Detected platform ('macos', 'linux', or 'windows')
32
+ * @returns Detected platform ('macos' or 'linux')
33
33
  * @throws PlatformDetectionError if detection fails or mixed platforms detected
34
34
  */
35
35
  getPlatform() {
@@ -84,7 +84,7 @@ export class PlatformDetector {
84
84
  return null;
85
85
  }
86
86
  const platformPrefix = parts[0];
87
- if (platformPrefix === 'macos' || platformPrefix === 'linux' || platformPrefix === 'windows') {
87
+ if (platformPrefix === 'macos' || platformPrefix === 'linux') {
88
88
  return platformPrefix;
89
89
  }
90
90
  return null;
@@ -45,25 +45,6 @@ describe('PlatformDetector', () => {
45
45
  expect(detector.getStack()).toBe('linux-docker-android-22.04');
46
46
  expect(detector.getMachineType()).toBe('standard');
47
47
  });
48
- test('should detect windows from stack', () => {
49
- const pipeline = {
50
- format_version: '4',
51
- meta: {
52
- 'cibuild.io': {
53
- stack: 'windows-server-2022',
54
- },
55
- },
56
- workflows: {
57
- primary: {
58
- steps: [{ 'script@1.0.0': { inputs: { content: 'echo "test"' } } }],
59
- },
60
- },
61
- };
62
- const detector = new PlatformDetector(pipeline, pipeline.workflows.primary, 'primary');
63
- expect(detector.getPlatform()).toBe('windows');
64
- expect(detector.getStack()).toBe('windows-server-2022');
65
- expect(detector.getMachineType()).toBeNull();
66
- });
67
48
  });
68
49
  describe('Priority 2: Use meta.platform field', () => {
69
50
  test('should use meta.platform when stack not specified', () => {
@@ -12,6 +12,8 @@ export interface CachePreset {
12
12
  lockfile: string;
13
13
  keyPrefix: string;
14
14
  paths: string[];
15
+ /** Optional fallback preset name: if primary lockfile not found, use this preset's lockfile/key/paths */
16
+ fallback?: string;
15
17
  }
16
18
  export declare const CACHE_PRESETS: Record<string, CachePreset>;
17
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CASrD,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAoGzF,iBAAiB;CAoEhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAwJzF,iBAAiB;CA0EhC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;AAoBF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAoGzF,iBAAiB;CA2FhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAwJzF,iBAAiB;CA6FhC"}
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { BaseStepExecutor } from './base.js';
5
5
  export const CACHE_PRESETS = {
6
+ ios: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'], fallback: 'spm' },
6
7
  cocoapods: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'] },
7
8
  carthage: { lockfile: 'Cartfile.resolved', keyPrefix: 'carthage', paths: ['Carthage'] },
8
9
  spm: { lockfile: 'Package.resolved', keyPrefix: 'spm', paths: ['~/Library/Developer/Xcode/DerivedData/*/SourcePackages'] },
@@ -12,6 +13,24 @@ export const CACHE_PRESETS = {
12
13
  yarn: { lockfile: 'yarn.lock', keyPrefix: 'yarn', paths: ['node_modules'] },
13
14
  dart: { lockfile: 'pubspec.lock', keyPrefix: 'dart', paths: ['.dart_tool', '.pub-cache'] },
14
15
  };
16
+ /**
17
+ * Resolves a preset with its fallback chain into a flat list of
18
+ * {lockfile, keyPrefix, paths} entries to try in order at runtime.
19
+ */
20
+ function resolvePresetChain(technology) {
21
+ const chain = [];
22
+ let current = technology;
23
+ const visited = new Set();
24
+ while (current && !visited.has(current)) {
25
+ visited.add(current);
26
+ const preset = CACHE_PRESETS[current];
27
+ if (!preset)
28
+ break;
29
+ chain.push(preset);
30
+ current = preset.fallback;
31
+ }
32
+ return chain;
33
+ }
15
34
  /**
16
35
  * Cache pull step executor
17
36
  * Restores cached files from cache directory
@@ -97,8 +116,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
97
116
  }
98
117
  async executeWithPreset(technology, inputs, config) {
99
118
  const stepName = 'cache-pull';
100
- const preset = CACHE_PRESETS[technology];
101
- if (!preset) {
119
+ const chain = resolvePresetChain(technology);
120
+ if (chain.length === 0) {
102
121
  const supported = Object.keys(CACHE_PRESETS).join(', ');
103
122
  throw new Error(`Unknown cache technology '${technology}'. Supported: ${supported}`);
104
123
  }
@@ -108,17 +127,39 @@ export class CachePullStepExecutor extends BaseStepExecutor {
108
127
  commands.push(`echo "Restoring ${technology} cache..."`);
109
128
  commands.push('');
110
129
  // Compute cache key from lockfile checksum at runtime
130
+ // If technology has a fallback chain, try each lockfile in order
111
131
  commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
112
132
  commands.push('mkdir -p "$CACHE_DIR"');
113
133
  commands.push('');
114
- commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
115
- commands.push('if [ -f "$LOCKFILE" ]; then');
116
- commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
117
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
118
- commands.push('else');
119
- commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
120
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
121
- commands.push('fi');
134
+ if (chain.length === 1) {
135
+ const preset = chain[0];
136
+ commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
137
+ commands.push('if [ -f "$LOCKFILE" ]; then');
138
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
139
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
140
+ commands.push('else');
141
+ commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
142
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
143
+ commands.push('fi');
144
+ }
145
+ else {
146
+ // Fallback chain: try each lockfile in order, use the first one found
147
+ for (let i = 0; i < chain.length; i++) {
148
+ const preset = chain[i];
149
+ const cond = i === 0 ? 'if' : 'elif';
150
+ commands.push(`${cond} [ -f "${this.escapeBash(preset.lockfile)}" ]; then`);
151
+ commands.push(` LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
152
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
153
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
154
+ if (isDebugMode) {
155
+ commands.push(` echo "Detected: ${this.escapeBash(preset.keyPrefix)} (lockfile: $LOCKFILE)"`);
156
+ }
157
+ }
158
+ commands.push('else');
159
+ commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
160
+ commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
161
+ commands.push('fi');
162
+ }
122
163
  commands.push('');
123
164
  if (isDebugMode) {
124
165
  commands.push('echo "Lockfile: $LOCKFILE"');
@@ -135,7 +176,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
135
176
  commands.push(' echo "Cache restored successfully"');
136
177
  if (isDebugMode) {
137
178
  commands.push(' echo "Restored paths:"');
138
- for (const p of preset.paths) {
179
+ const allPaths = [...new Set(chain.flatMap(p => p.paths))];
180
+ for (const p of allPaths) {
139
181
  commands.push(` EXPANDED="${this.escapeBash(p)}"`);
140
182
  commands.push(' EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
141
183
  commands.push(' if [ -e "$EXPANDED" ]; then');
@@ -285,8 +327,8 @@ export class CachePushStepExecutor extends BaseStepExecutor {
285
327
  }
286
328
  async executeWithPreset(technology, inputs, config) {
287
329
  const stepName = 'cache-push';
288
- const preset = CACHE_PRESETS[technology];
289
- if (!preset) {
330
+ const chain = resolvePresetChain(technology);
331
+ if (chain.length === 0) {
290
332
  const supported = Object.keys(CACHE_PRESETS).join(', ');
291
333
  throw new Error(`Unknown cache technology '${technology}'. Supported: ${supported}`);
292
334
  }
@@ -299,14 +341,31 @@ export class CachePushStepExecutor extends BaseStepExecutor {
299
341
  commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
300
342
  commands.push('mkdir -p "$CACHE_DIR"');
301
343
  commands.push('');
302
- commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
303
- commands.push('if [ -f "$LOCKFILE" ]; then');
304
- commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
305
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
306
- commands.push('else');
307
- commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
308
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
309
- commands.push('fi');
344
+ if (chain.length === 1) {
345
+ const preset = chain[0];
346
+ commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
347
+ commands.push('if [ -f "$LOCKFILE" ]; then');
348
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
349
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
350
+ commands.push('else');
351
+ commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
352
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
353
+ commands.push('fi');
354
+ }
355
+ else {
356
+ for (let i = 0; i < chain.length; i++) {
357
+ const preset = chain[i];
358
+ const cond = i === 0 ? 'if' : 'elif';
359
+ commands.push(`${cond} [ -f "${this.escapeBash(preset.lockfile)}" ]; then`);
360
+ commands.push(` LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
361
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
362
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
363
+ }
364
+ commands.push('else');
365
+ commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
366
+ commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
367
+ commands.push('fi');
368
+ }
310
369
  commands.push('');
311
370
  if (isDebugMode) {
312
371
  commands.push('echo "Lockfile: $LOCKFILE"');
@@ -314,10 +373,12 @@ export class CachePushStepExecutor extends BaseStepExecutor {
314
373
  }
315
374
  commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
316
375
  commands.push('');
317
- // Collect paths to cache
376
+ // Collect paths to cache — use all paths from all presets in the chain,
377
+ // only archiving the ones that actually exist on disk
318
378
  commands.push('# Collect paths to cache');
319
379
  commands.push('PATHS_TO_CACHE=()');
320
- for (const p of preset.paths) {
380
+ const allPaths = [...new Set(chain.flatMap(p => p.paths))];
381
+ for (const p of allPaths) {
321
382
  commands.push(`EXPANDED="${this.escapeBash(p)}"`);
322
383
  commands.push('EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
323
384
  commands.push('if [ -e "$EXPANDED" ]; then');
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * TypeScript interfaces for Bitrise-compatible YAML pipeline structure
3
3
  */
4
- export type Platform = 'macos' | 'linux' | 'windows';
4
+ export type Platform = 'macos' | 'linux';
5
5
  export type MachineType = 'standard' | 'performance';
6
6
  export interface YAMLEnvVar {
7
7
  [key: string]: string;
@@ -10,7 +10,8 @@ export interface YAMLCIBuildMeta {
10
10
  stack?: string;
11
11
  machine_type?: MachineType;
12
12
  machine_type_id?: string;
13
- platform?: 'ios' | 'android';
13
+ platform?: 'ios' | 'android' | 'kmm';
14
+ cachedBuild?: boolean;
14
15
  }
15
16
  export interface YAMLMeta {
16
17
  'cibuild.io'?: YAMLCIBuildMeta;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/yaml/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AACrD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,aAAa,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAGD,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,WAAW,QAAQ;IACvB,CAAC,mBAAmB,EAAE,MAAM,GAAG;QAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;CACH;AAGD,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAGD,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAGD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAGD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAGD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/yaml/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,aAAa,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAGD,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,WAAW,QAAQ;IACvB,CAAC,mBAAmB,EAAE,MAAM,GAAG;QAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;CACH;AAGD,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAGD,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAGD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAGD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAGD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.3.19",
3
+ "version": "1.4.1",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",