@invarn/cibuild 2.7.4 → 2.7.5

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.
Files changed (52) hide show
  1. package/dist/cli.cjs +10 -10
  2. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
  3. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
  4. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +219 -0
  5. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
  6. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
  7. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +291 -0
  8. package/dist/src/commands/build.d.ts +124 -1
  9. package/dist/src/commands/build.d.ts.map +1 -1
  10. package/dist/src/commands/build.js +251 -33
  11. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
  12. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
  13. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
  14. package/dist/src/commands/file-secret-collector.d.ts +13 -0
  15. package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
  16. package/dist/src/commands/file-secret-collector.js +3 -2
  17. package/dist/src/commands/local-properties-reaches-every-workflow.test.js +2 -0
  18. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
  19. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
  20. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
  21. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
  22. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
  23. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
  24. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
  25. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
  26. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
  27. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
  28. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
  29. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
  30. package/dist/src/yaml/converter.d.ts +48 -0
  31. package/dist/src/yaml/converter.d.ts.map +1 -1
  32. package/dist/src/yaml/converter.js +117 -0
  33. package/dist/src/yaml/env-resolver.d.ts +52 -0
  34. package/dist/src/yaml/env-resolver.d.ts.map +1 -1
  35. package/dist/src/yaml/env-resolver.js +99 -1
  36. package/dist/src/yaml/secrets-manager.d.ts +13 -3
  37. package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
  38. package/dist/src/yaml/secrets-manager.js +25 -14
  39. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
  40. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
  41. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
  42. package/dist/src/yaml/steps/index.d.ts.map +1 -1
  43. package/dist/src/yaml/steps/index.js +9 -0
  44. package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
  45. package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
  46. package/dist/src/yaml/steps/ios-deps.js +18 -3
  47. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
  48. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
  49. package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
  50. package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
  51. package/dist/src/yaml/steps/xcode.js +73 -20
  52. package/package.json +1 -1
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `cocoapods-install` can be asked to skip a project that has no Podfile.
3
+ *
4
+ * The default stays a hard failure, because a pipeline that names this step is
5
+ * asserting the project uses CocoaPods and a missing Podfile is a real error.
6
+ *
7
+ * The iOS template needs the other behaviour: it serves every iOS project and a
8
+ * large share of them resolve dependencies through Swift Package Manager, where
9
+ * "Podfile not found" would be a CocoaPods error in a project that does not use
10
+ * CocoaPods. Until this input existed the template could not use this step at
11
+ * all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
12
+ * of reach of the refused-fetch retry this step carries. That is the gap this
13
+ * closes, and it is why the release order matters: this must ship before the
14
+ * template is pointed at the step, or every SPM-only iOS repo breaks.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=cocoapods-skip-if-absent.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cocoapods-skip-if-absent.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cocoapods-skip-if-absent.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `cocoapods-install` can be asked to skip a project that has no Podfile.
3
+ *
4
+ * The default stays a hard failure, because a pipeline that names this step is
5
+ * asserting the project uses CocoaPods and a missing Podfile is a real error.
6
+ *
7
+ * The iOS template needs the other behaviour: it serves every iOS project and a
8
+ * large share of them resolve dependencies through Swift Package Manager, where
9
+ * "Podfile not found" would be a CocoaPods error in a project that does not use
10
+ * CocoaPods. Until this input existed the template could not use this step at
11
+ * all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
12
+ * of reach of the refused-fetch retry this step carries. That is the gap this
13
+ * closes, and it is why the release order matters: this must ship before the
14
+ * template is pointed at the step, or every SPM-only iOS repo breaks.
15
+ */
16
+ import { describe, test, expect } from '@jest/globals';
17
+ import { CocoapodsInstallStepExecutor } from './ios-deps.js';
18
+ import { testConfig } from './test-config.js';
19
+ const build = async (inputs = {}) => {
20
+ const executor = new CocoapodsInstallStepExecutor();
21
+ const result = await executor.execute(inputs, {}, testConfig);
22
+ return result.script;
23
+ };
24
+ /** The body of the `if [ ! -f "Podfile" ]` branch, which is what changes. */
25
+ const absentBranch = (script) => {
26
+ const lines = script.split('\n');
27
+ const start = lines.findIndex((l) => l.includes('if [ ! -f "Podfile" ]'));
28
+ expect(start).toBeGreaterThanOrEqual(0);
29
+ const end = lines.findIndex((l, i) => i > start && l.trim() === 'fi');
30
+ return lines.slice(start, end + 1).join('\n');
31
+ };
32
+ describe('by default a missing Podfile is still an error', () => {
33
+ test('exits non-zero', async () => {
34
+ expect(absentBranch(await build())).toContain('exit 1');
35
+ });
36
+ test('says what was not found', async () => {
37
+ expect(absentBranch(await build())).toContain('Podfile not found');
38
+ });
39
+ test('an explicit false is the same as omitting it', async () => {
40
+ expect(absentBranch(await build({ skip_if_absent: false }))).toContain('exit 1');
41
+ });
42
+ });
43
+ describe('skip_if_absent turns it into a clean no-op', () => {
44
+ test('exits zero instead of one', async () => {
45
+ const branch = absentBranch(await build({ skip_if_absent: true }));
46
+ expect(branch).toContain('exit 0');
47
+ expect(branch).not.toContain('exit 1');
48
+ });
49
+ test('says it skipped rather than that something failed', async () => {
50
+ const branch = absentBranch(await build({ skip_if_absent: true }));
51
+ expect(branch).toContain('skipping CocoaPods install');
52
+ expect(branch).not.toContain('Error');
53
+ });
54
+ test('still runs pod install when a Podfile IS present', async () => {
55
+ // The skip is a branch, not a mode — the rest of the step is untouched.
56
+ const script = await build({ skip_if_absent: true });
57
+ expect(script).toContain('POD_INVOCATION');
58
+ expect(script).toContain('$POD_INVOCATION $POD_CMD');
59
+ });
60
+ test('still wraps the pod fetch in the refused-fetch retry', async () => {
61
+ // This is the entire point of letting the template use the step.
62
+ const script = await build({ skip_if_absent: true });
63
+ expect(script).toContain('cibuild_fetch_was_refused');
64
+ expect(script).toContain('CIBUILD_FETCH_MAX_ATTEMPTS');
65
+ });
66
+ test('still chooses between bundle exec and bare pod', async () => {
67
+ const script = await build({ skip_if_absent: true });
68
+ expect(script).toContain('USE_BUNDLE_EXEC');
69
+ expect(script).toContain('bundle exec pod');
70
+ });
71
+ });
72
+ /**
73
+ * The inline script the template used did two things, not one: it skipped a
74
+ * project with no Podfile, AND it looked for that Podfile beside the Xcode
75
+ * project rather than at the repository root. Both have to exist as inputs or
76
+ * the template cannot be switched — about a quarter of iOS repositories keep
77
+ * the app one directory down, and template inputs interpolate variables, not
78
+ * `$(dirname …)`.
79
+ */
80
+ describe('project_path finds the Podfile beside the project', () => {
81
+ test('changes into the project directory, not the repo root', async () => {
82
+ const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
83
+ expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
84
+ expect(script).toContain('cd "$WORK_DIR"');
85
+ });
86
+ test('does not fall back to CIBUILD_SOURCE_DIR when it is given', async () => {
87
+ const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
88
+ expect(script).not.toContain('cd "$CIBUILD_SOURCE_DIR"');
89
+ });
90
+ test('still falls back to CIBUILD_SOURCE_DIR when nothing is given', async () => {
91
+ expect(await build()).toContain('cd "$CIBUILD_SOURCE_DIR"');
92
+ });
93
+ test('an explicit source_root_path still wins over project_path', async () => {
94
+ const script = await build({
95
+ project_path: 'ios/MyApp.xcworkspace',
96
+ source_root_path: 'somewhere/else',
97
+ });
98
+ expect(script).toContain(`cd 'somewhere/else'`);
99
+ expect(script).not.toContain('dirname');
100
+ });
101
+ test('composes with skip_if_absent, which is how the template uses it', async () => {
102
+ const script = await build({ project_path: 'ios/MyApp.xcworkspace', skip_if_absent: true });
103
+ expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
104
+ expect(absentBranch(script)).toContain('exit 0');
105
+ expect(script).toContain('cibuild_fetch_was_refused');
106
+ });
107
+ });
108
+ /**
109
+ * The exact input combination the iOS template sends. Pinned here because the
110
+ * template lives in another repository and deploys on a different cadence, so
111
+ * nothing else would notice this step drifting away from what it sends.
112
+ */
113
+ describe('the iOS template combination', () => {
114
+ const templateInputs = {
115
+ project_path: 'MyApp.xcworkspace',
116
+ skip_if_absent: true,
117
+ command: 'install --repo-update',
118
+ };
119
+ test('produces pod install --repo-update, flags intact', async () => {
120
+ const script = await build(templateInputs);
121
+ expect(script).toContain('POD_CMD="install --repo-update"');
122
+ });
123
+ test('runs that under the refused-fetch retry, exactly once', async () => {
124
+ const script = await build(templateInputs);
125
+ expect(script).toContain('$POD_INVOCATION $POD_CMD');
126
+ expect(script.split('CIBUILD_FETCH_MAX_ATTEMPTS=').length - 1).toBe(1);
127
+ });
128
+ test('skips instead of failing when the repo has no Podfile', async () => {
129
+ expect(absentBranch(await build(templateInputs))).toContain('exit 0');
130
+ });
131
+ });
132
+ //# sourceMappingURL=cocoapods-skip-if-absent.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CA07C7C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAq8C7C"}
@@ -451,11 +451,20 @@ export function initializeStepRegistry() {
451
451
  description: 'Explicit path to Podfile',
452
452
  required: false,
453
453
  },
454
+ project_path: {
455
+ description: 'A .xcodeproj/.xcworkspace whose directory holds the Podfile. For repositories that keep the app one directory down from the root.',
456
+ required: false,
457
+ },
454
458
  verbose: {
455
459
  description: 'Enable verbose CocoaPods logging',
456
460
  required: false,
457
461
  default: false,
458
462
  },
463
+ skip_if_absent: {
464
+ description: 'Succeed and do nothing when there is no Podfile, instead of failing. For pipelines that serve every iOS project, where plenty use Swift Package Manager and have no Podfile.',
465
+ required: false,
466
+ default: false,
467
+ },
459
468
  },
460
469
  });
461
470
  registerStep('carthage', new CarthageStepExecutor(), {
@@ -11,8 +11,43 @@ export interface CocoapodsInstallInputs {
11
11
  source_root_path?: string;
12
12
  /** Explicit path to Podfile (overrides source_root_path) */
13
13
  podfile_path?: string;
14
+ /**
15
+ * A .xcodeproj/.xcworkspace whose DIRECTORY holds the Podfile.
16
+ *
17
+ * About a quarter of iOS repositories keep the app one directory down from
18
+ * the repository root, and the Podfile is down there with it — so "run pod
19
+ * install at the root" is wrong for them. A caller that already knows where
20
+ * the project is should not have to compute its directory separately, and a
21
+ * pipeline template cannot: template inputs interpolate variables, not
22
+ * `$(dirname …)`, so `source_root_path` alone could not express it and the
23
+ * iOS template had to run `pod install` from an inline script to get at
24
+ * `dirname`. That inline script is what put it out of reach of the
25
+ * refused-fetch retry.
26
+ *
27
+ * Only the directory is used; the file itself need not exist.
28
+ *
29
+ * Precedence, most specific first: podfile_path, source_root_path,
30
+ * project_path, then $CIBUILD_SOURCE_DIR.
31
+ */
32
+ project_path?: string;
14
33
  /** Enable verbose CocoaPods logging */
15
34
  verbose?: boolean;
35
+ /**
36
+ * Succeed and do nothing when there is no Podfile, instead of failing.
37
+ *
38
+ * Defaults to false, which keeps the contract every existing caller was
39
+ * written against: a pipeline that names this step is asserting the project
40
+ * uses CocoaPods, and a missing Podfile is a real error worth stopping for.
41
+ *
42
+ * The iOS template is the caller that needs the other behaviour. It serves
43
+ * every iOS project, and a large share of them resolve dependencies through
44
+ * Swift Package Manager and have no Podfile at all — for those, "Podfile not
45
+ * found" is a CocoaPods error in a project that does not use CocoaPods.
46
+ * Until this existed the template could not use this step, and so ran
47
+ * `pod install` from an inline script instead, out of reach of the
48
+ * refused-fetch retry that this step carries.
49
+ */
50
+ skip_if_absent?: boolean;
16
51
  }
17
52
  /**
18
53
  * Runs `pod install` or `pod update`.
@@ -1 +1 @@
1
- {"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAkFpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
1
+ {"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA+FpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
@@ -19,7 +19,9 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
19
19
  const command = this.getInput(inputs, 'command', 'install');
20
20
  const sourceRootPath = this.getInput(inputs, 'source_root_path', '');
21
21
  const podfilePath = this.getInput(inputs, 'podfile_path', '');
22
+ const projectPath = this.getInput(inputs, 'project_path', '');
22
23
  const verbose = this.getInput(inputs, 'verbose', false);
24
+ const skipIfAbsent = this.getInput(inputs, 'skip_if_absent', false);
23
25
  const commands = [];
24
26
  commands.push('# cocoapods-install');
25
27
  commands.push('echo "🔧 Running CocoaPods..."');
@@ -33,6 +35,11 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
33
35
  else if (sourceRootPath) {
34
36
  commands.push(`cd '${this.escapeBash(sourceRootPath)}'`);
35
37
  }
38
+ else if (projectPath) {
39
+ commands.push('# Podfile sits beside the Xcode project, not at the repo root');
40
+ commands.push(`WORK_DIR="$(dirname '${this.escapeBash(projectPath)}')"`);
41
+ commands.push('cd "$WORK_DIR"');
42
+ }
36
43
  else {
37
44
  commands.push('# Use CIBUILD_SOURCE_DIR if set, otherwise current directory');
38
45
  commands.push('if [ -n "$CIBUILD_SOURCE_DIR" ]; then');
@@ -40,10 +47,18 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
40
47
  commands.push('fi');
41
48
  }
42
49
  commands.push('');
43
- // Check for Podfile
50
+ // Check for Podfile. Absent is either a hard error or a clean no-op,
51
+ // depending on whether the caller is asserting "this project uses
52
+ // CocoaPods" or asking "use CocoaPods if this project does".
44
53
  commands.push('if [ ! -f "Podfile" ]; then');
45
- commands.push(' echo "❌ Error: Podfile not found in $(pwd)"');
46
- commands.push(' exit 1');
54
+ if (skipIfAbsent) {
55
+ commands.push(' echo "No Podfile in $(pwd) — skipping CocoaPods install."');
56
+ commands.push(' exit 0');
57
+ }
58
+ else {
59
+ commands.push(' echo "❌ Error: Podfile not found in $(pwd)"');
60
+ commands.push(' exit 1');
61
+ }
47
62
  commands.push('fi');
48
63
  commands.push('');
49
64
  // Determine whether to use bundle exec
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Products go under a derived data path, never into one flat directory.
3
+ *
4
+ * `CONFIGURATION_BUILD_DIR` overrides the per-configuration/per-platform
5
+ * product separation xcodebuild provides under
6
+ * `DerivedData/Build/Products/<config>-<platform>/`, flattening every target in
7
+ * the graph into a single directory. Two build tasks for the same target then
8
+ * declare the same absolute output path and xcodebuild refuses the entire build
9
+ * with "Multiple commands produce". The notes it prints name the SAME target
10
+ * twice, which is the tell: one target built twice into one directory, not two
11
+ * packages colliding on a name.
12
+ *
13
+ * Measured 2026-09-09 on the iOS corpus rows, all on the template's must-pass
14
+ * "Build App" step: 16 collisions on jellyfin/Swiftfin, 266 on
15
+ * Automattic/pocket-casts-ios, 425 on home-assistant/iOS.
16
+ *
17
+ * A fourth row failed differently for the same reason. CocoaPods derives
18
+ * `PODS_CONFIGURATION_BUILD_DIR` from `BUILD_DIR`, which `CONFIGURATION_BUILD_DIR`
19
+ * does NOT override — so pod products land in the flat directory while every
20
+ * generated copy-resources script and linker search path still points into
21
+ * DerivedData. shenruisi/Stay reported "Resource SDWebImage.bundle not found.
22
+ * Run pod install" for a bundle that had been built two directories away.
23
+ *
24
+ * Proved by control on a runner guest, same repo and image and flags with only
25
+ * this setting differing: `CONFIGURATION_BUILD_DIR` reproduced the collision and
26
+ * exit 65 with no .app, `-derivedDataPath` reached BUILD SUCCEEDED in 2m06s and
27
+ * left both `Debug` and `Debug-iphonesimulator` under Products.
28
+ */
29
+ export {};
30
+ //# sourceMappingURL=xcode-derived-data.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcode-derived-data.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-derived-data.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Products go under a derived data path, never into one flat directory.
3
+ *
4
+ * `CONFIGURATION_BUILD_DIR` overrides the per-configuration/per-platform
5
+ * product separation xcodebuild provides under
6
+ * `DerivedData/Build/Products/<config>-<platform>/`, flattening every target in
7
+ * the graph into a single directory. Two build tasks for the same target then
8
+ * declare the same absolute output path and xcodebuild refuses the entire build
9
+ * with "Multiple commands produce". The notes it prints name the SAME target
10
+ * twice, which is the tell: one target built twice into one directory, not two
11
+ * packages colliding on a name.
12
+ *
13
+ * Measured 2026-09-09 on the iOS corpus rows, all on the template's must-pass
14
+ * "Build App" step: 16 collisions on jellyfin/Swiftfin, 266 on
15
+ * Automattic/pocket-casts-ios, 425 on home-assistant/iOS.
16
+ *
17
+ * A fourth row failed differently for the same reason. CocoaPods derives
18
+ * `PODS_CONFIGURATION_BUILD_DIR` from `BUILD_DIR`, which `CONFIGURATION_BUILD_DIR`
19
+ * does NOT override — so pod products land in the flat directory while every
20
+ * generated copy-resources script and linker search path still points into
21
+ * DerivedData. shenruisi/Stay reported "Resource SDWebImage.bundle not found.
22
+ * Run pod install" for a bundle that had been built two directories away.
23
+ *
24
+ * Proved by control on a runner guest, same repo and image and flags with only
25
+ * this setting differing: `CONFIGURATION_BUILD_DIR` reproduced the collision and
26
+ * exit 65 with no .app, `-derivedDataPath` reached BUILD SUCCEEDED in 2m06s and
27
+ * left both `Debug` and `Debug-iphonesimulator` under Products.
28
+ */
29
+ import { describe, test, expect } from '@jest/globals';
30
+ import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor, XcodeBuildForSimulatorStepExecutor, } from './xcode.js';
31
+ import { testConfig } from './test-config.js';
32
+ const DERIVED = '-derivedDataPath "$OUTPUT_DIR/DerivedData"';
33
+ const scriptFor = async (executor, inputs) => {
34
+ const result = await executor.execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp', ...inputs }, {}, testConfig);
35
+ return result.script;
36
+ };
37
+ const buildingExecutors = () => [
38
+ ['xcodebuild', new XcodeBuildStepExecutor()],
39
+ ['xcode-build-for-test', new XcodeBuildForTestStepExecutor()],
40
+ ['xcode-build-for-simulator', new XcodeBuildForSimulatorStepExecutor()],
41
+ ];
42
+ describe('no xcode step flattens the product tree', () => {
43
+ test.each(buildingExecutors())('%s never passes CONFIGURATION_BUILD_DIR', async (_name, executor) => {
44
+ expect(await scriptFor(executor, {})).not.toContain('CONFIGURATION_BUILD_DIR');
45
+ });
46
+ test.each(buildingExecutors())('%s builds into a derived data path', async (_name, executor) => {
47
+ expect(await scriptFor(executor, {})).toContain(DERIVED);
48
+ });
49
+ });
50
+ describe('the resolve and the build read the same derived data', () => {
51
+ // If they diverge, the retried resolve populates one location and the build
52
+ // re-resolves into another — the retry is spent on work that is thrown away,
53
+ // and a refused fetch fails the expensive half anyway.
54
+ test.each(buildingExecutors())('%s', async (_name, executor) => {
55
+ const script = await scriptFor(executor, {});
56
+ const lines = script.split('\n');
57
+ const resolveLine = lines.find((l) => l.includes('-resolvePackageDependencies'));
58
+ const buildLine = lines.find((l) => /^xcodebuild (build|clean build|build-for-testing)/.test(l.trim()));
59
+ expect(resolveLine).toBeDefined();
60
+ expect(buildLine).toBeDefined();
61
+ expect(resolveLine).toContain(DERIVED);
62
+ expect(buildLine).toContain(DERIVED);
63
+ });
64
+ });
65
+ describe('xcode-build-for-simulator finds the .app where it now lands', () => {
66
+ // The guard is the step's strongest statement that the app compiled, so it
67
+ // has to follow the products. Left at `find "$OUTPUT_DIR" -maxdepth 1` it
68
+ // matches nothing and fails every build with ".app not found" while the app
69
+ // has in fact built perfectly.
70
+ const build = (inputs = {}) => scriptFor(new XcodeBuildForSimulatorStepExecutor(), inputs);
71
+ test('searches under the per-platform Products directory', async () => {
72
+ const script = await build();
73
+ expect(script).toContain('find "$OUTPUT_DIR/DerivedData/Build/Products"');
74
+ });
75
+ test('searches deep enough to reach <config>-<platform>/Foo.app', async () => {
76
+ // Products/Debug-iphonesimulator/MyApp.app is two levels down. maxdepth 1
77
+ // would find the configuration directory and no .app at all.
78
+ const findLine = (await build())
79
+ .split('\n')
80
+ .find((l) => l.includes('-name "*.app"'));
81
+ expect(findLine).toContain('-maxdepth 2');
82
+ });
83
+ test('still fails the step when nothing landed', async () => {
84
+ const script = await build();
85
+ expect(script).toContain('if [ -z "$APP_PATH" ]; then');
86
+ expect(script).toContain('exit 1');
87
+ });
88
+ test('still exports the app path for later steps', async () => {
89
+ expect(await build()).toContain('envman add --key CIBUILD_APP_DIR_PATH');
90
+ });
91
+ });
92
+ describe('the output_dir input still steers where everything goes', () => {
93
+ // The template gives "Build App" its own output_dir, so derived data has to
94
+ // follow that input rather than a hardcoded path.
95
+ test('a custom output_dir is what OUTPUT_DIR resolves to', async () => {
96
+ const script = await scriptFor(new XcodeBuildForSimulatorStepExecutor(), {
97
+ output_dir: 'build/app',
98
+ });
99
+ expect(script).toContain(`OUTPUT_DIR="$(cd 'build/app' && pwd)"`);
100
+ expect(script).toContain(DERIVED);
101
+ });
102
+ });
103
+ //# sourceMappingURL=xcode-derived-data.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
@@ -52,10 +52,13 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
52
52
  commands.push(`echo "Building project: ${this.escapeBash(projectPath)}"`);
53
53
  commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
54
54
  commands.push(`echo "Configuration: ${this.escapeBash(configuration)}"`);
55
- // Create output directory and resolve to absolute path
56
- // Using an absolute path for CONFIGURATION_BUILD_DIR prevents xcodebuild from
57
- // creating a 'build' directory inside each SPM package checkout in DerivedData,
58
- // which causes "File exists but is not a directory" errors on repeated runs.
55
+ // Create output directory and resolve to absolute path. The absolute path
56
+ // is the load-bearing part: a relative one let xcodebuild create a `build`
57
+ // directory inside each SPM package checkout in DerivedData, which fails
58
+ // with "File exists but is not a directory" on repeated runs. That was
59
+ // originally fixed by making CONFIGURATION_BUILD_DIR absolute; it is now
60
+ // -derivedDataPath that is absolute instead, which fixes the same thing
61
+ // without flattening the product tree.
59
62
  commands.push('');
60
63
  commands.push('# Create output directory and resolve to absolute path');
61
64
  commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
@@ -81,9 +84,10 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
81
84
  commands.push('fi');
82
85
  // Resolve the package graph before building, and retry that on a refused
83
86
  // fetch. Resolution is seconds and the build is minutes, so the retry is
84
- // spent on the cheap half. No -derivedDataPath here: this step does not
85
- // set one for the build either, so both use the default location and the
86
- // build finds the graph this just resolved.
87
+ // spent on the cheap half. The two -derivedDataPath arguments move
88
+ // together: the build below reads the same derived data and finds the
89
+ // graph already resolved. Split them and the retry is spent on work the
90
+ // build discards.
87
91
  commands.push('');
88
92
  commands.push('# Resolve package dependencies first — see git-fetch-retry');
89
93
  commands.push(...refusedFetchHelpers());
@@ -91,7 +95,8 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
91
95
  label: 'Package resolution',
92
96
  command: 'xcodebuild -resolvePackageDependencies' +
93
97
  ` "$PROJECT_TYPE" '${escapedPath}'` +
94
- ` -scheme '${this.escapeBash(scheme)}'`,
98
+ ` -scheme '${this.escapeBash(scheme)}'` +
99
+ ' -derivedDataPath "$OUTPUT_DIR/DerivedData"',
95
100
  }));
96
101
  // Build xcodebuild command
97
102
  commands.push('');
@@ -106,8 +111,14 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
106
111
  xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
107
112
  xcodebuildCmd += ` -configuration '${this.escapeBash(configuration)}'`;
108
113
  xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
109
- // Add build path (absolute, resolved above)
110
- xcodebuildCmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
114
+ // Derived data under the output dir, rather than CONFIGURATION_BUILD_DIR.
115
+ // Same reasoning as XcodeBuildForSimulatorStepExecutor below, where the
116
+ // full mechanism and the control are written up: the flat directory
117
+ // collapses xcodebuild's per-configuration/per-platform product separation
118
+ // and any graph with two tasks producing the same output name is refused
119
+ // with "Multiple commands produce". It also desynchronises CocoaPods, whose
120
+ // PODS_CONFIGURATION_BUILD_DIR follows BUILD_DIR and not this setting.
121
+ xcodebuildCmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
111
122
  // Add xcconfig if provided
112
123
  if (xcconfigContent && xcconfigContent.trim() !== '') {
113
124
  xcodebuildCmd += ' -xcconfig "$XCCONFIG_FILE"';
@@ -117,8 +128,11 @@ export class XcodeBuildStepExecutor extends BaseStepExecutor {
117
128
  commands.push('');
118
129
  commands.push('# Build completed');
119
130
  commands.push('echo "Build completed successfully"');
120
- commands.push('echo "Build artifacts in: $OUTPUT_DIR"');
121
- commands.push('ls -lh "$OUTPUT_DIR" || true');
131
+ // Products live under the per-platform Products directory now, not flat in
132
+ // $OUTPUT_DIR, so point the listing at where they actually are — otherwise
133
+ // the log says "artifacts in build/" above an empty directory.
134
+ commands.push('echo "Build artifacts in: $OUTPUT_DIR/DerivedData/Build/Products"');
135
+ commands.push('ls -lhR "$OUTPUT_DIR/DerivedData/Build/Products" 2>/dev/null || true');
122
136
  const script = this.createBashScriptFromCommands(commands, stepName);
123
137
  return this.createScriptStep(script, stepName);
124
138
  }
@@ -1013,16 +1027,19 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
1013
1027
  commands.push('');
1014
1028
  // Resolve the package graph before building, under the refused-fetch
1015
1029
  // retry. This is the step an iOS app is normally built with, so it is the
1016
- // one a refused dependency fetch lands on first. No -derivedDataPath: the
1017
- // build below does not set one either, so both use the default location
1018
- // and the build finds the graph this just resolved.
1030
+ // one a refused dependency fetch lands on first. The build below reads the
1031
+ // same derived data and finds the graph already resolved — the two
1032
+ // -derivedDataPath arguments have to move together, or the resolve
1033
+ // populates one location and the build re-resolves into another, which
1034
+ // silently spends the retry on work the build then throws away.
1019
1035
  commands.push('# Resolve package dependencies first — see git-fetch-retry');
1020
1036
  commands.push(...refusedFetchHelpers());
1021
1037
  commands.push(...retryOnRefusedFetch({
1022
1038
  label: 'Package resolution',
1023
1039
  command: 'xcodebuild -resolvePackageDependencies' +
1024
1040
  ` "$PROJECT_TYPE" '${escapedPath}'` +
1025
- ` -scheme '${escapedScheme}'`,
1041
+ ` -scheme '${escapedScheme}'` +
1042
+ ' -derivedDataPath "$OUTPUT_DIR/DerivedData"',
1026
1043
  }));
1027
1044
  // Build command
1028
1045
  let cmd = 'xcodebuild';
@@ -1036,18 +1053,54 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
1036
1053
  cmd += ` -configuration '${this.escapeBash(configuration)}'`;
1037
1054
  }
1038
1055
  cmd += ` -destination '${this.escapeBash(destination)}'`;
1039
- cmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
1056
+ // -derivedDataPath rather than CONFIGURATION_BUILD_DIR. The latter was set
1057
+ // to keep xcodebuild from writing a `build` directory inside each SPM
1058
+ // package checkout in DerivedData ("File exists but is not a directory" on
1059
+ // repeated runs) — but note what that fix actually was: making the path
1060
+ // ABSOLUTE. Pointing derived data somewhere writable achieves the same
1061
+ // thing without the cost below, and matches xcode-build-for-test.
1062
+ //
1063
+ // The cost, measured 2026-09-09 on four of the six iOS corpus rows:
1064
+ // CONFIGURATION_BUILD_DIR overrides the per-configuration/per-platform
1065
+ // product separation xcodebuild normally provides under
1066
+ // DerivedData/Build/Products/<config>-<platform>/, flattening every target
1067
+ // in the graph into ONE directory. Two build tasks for the same target then
1068
+ // declare the same absolute output path and xcodebuild refuses the whole
1069
+ // build with "Multiple commands produce". Read the notes it prints: they
1070
+ // name the SAME target twice. It is not two packages colliding on a name,
1071
+ // it is one target built twice into one directory. 16 collisions on
1072
+ // jellyfin/Swiftfin, 266 on Automattic/pocket-casts-ios, 425 on
1073
+ // home-assistant/iOS.
1074
+ //
1075
+ // It also breaks CocoaPods, which is the same defect wearing a disguise.
1076
+ // CocoaPods derives PODS_CONFIGURATION_BUILD_DIR from BUILD_DIR, and
1077
+ // BUILD_DIR is NOT what CONFIGURATION_BUILD_DIR overrides — so pod products
1078
+ // are written to the flat directory while every generated copy-resources
1079
+ // script and linker search path still points into DerivedData. On
1080
+ // shenruisi/Stay that surfaced as "Resource SDWebImage.bundle not found.
1081
+ // Run pod install", for a bundle that had in fact been built, two
1082
+ // directories away.
1083
+ //
1084
+ // Proved by control, same repo and image and flags, only this line
1085
+ // differing: CONFIGURATION_BUILD_DIR reproduced the collision and exit 65
1086
+ // with no .app; -derivedDataPath reached BUILD SUCCEEDED in 2m06s and left
1087
+ // both Debug and Debug-iphonesimulator under Products — the separation the
1088
+ // flat directory destroys.
1089
+ cmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
1040
1090
  cmd += ' -xcconfig "$XCCONFIG_FILE"';
1041
1091
  if (xcodebuildOptions) {
1042
1092
  cmd += ` ${xcodebuildOptions}`;
1043
1093
  }
1044
1094
  commands.push(cmd);
1045
1095
  commands.push('');
1046
- // Locate .app
1096
+ // Locate .app. It now lands under the per-platform Products directory
1097
+ // rather than flat in $OUTPUT_DIR, so the guard has to follow it — at
1098
+ // maxdepth 1 this found nothing and the step would fail every build with
1099
+ // ".app not found" while the app had compiled perfectly.
1047
1100
  commands.push('# Locate generated .app');
1048
- commands.push('APP_PATH=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.app" -type d | head -1)');
1101
+ commands.push('APP_PATH=$(find "$OUTPUT_DIR/DerivedData/Build/Products" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)');
1049
1102
  commands.push('if [ -z "$APP_PATH" ]; then');
1050
- commands.push(' echo "❌ Error: .app not found in $OUTPUT_DIR"');
1103
+ commands.push(' echo "❌ Error: .app not found under $OUTPUT_DIR/DerivedData/Build/Products"');
1051
1104
  commands.push(' exit 1');
1052
1105
  commands.push('fi');
1053
1106
  commands.push('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.7.4",
3
+ "version": "2.7.5",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",