@invarn/cibuild 1.3.16 → 1.3.17

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 (242) hide show
  1. package/dist/cli.cjs +1 -1
  2. package/dist/src/cli.d.ts +3 -0
  3. package/dist/src/cli.d.ts.map +1 -0
  4. package/dist/src/cli.js +987 -0
  5. package/dist/src/commands/android-scanner.d.ts +32 -0
  6. package/dist/src/commands/android-scanner.d.ts.map +1 -0
  7. package/dist/src/commands/android-scanner.js +667 -0
  8. package/dist/src/commands/build.d.ts +5 -0
  9. package/dist/src/commands/build.d.ts.map +1 -0
  10. package/dist/src/commands/build.js +1096 -0
  11. package/dist/src/commands/edit.d.ts +3 -0
  12. package/dist/src/commands/edit.d.ts.map +1 -0
  13. package/dist/src/commands/edit.js +651 -0
  14. package/dist/src/commands/file-secret-collector.d.ts +37 -0
  15. package/dist/src/commands/file-secret-collector.d.ts.map +1 -0
  16. package/dist/src/commands/file-secret-collector.js +199 -0
  17. package/dist/src/commands/github-workflow.d.ts +5 -0
  18. package/dist/src/commands/github-workflow.d.ts.map +1 -0
  19. package/dist/src/commands/github-workflow.js +45 -0
  20. package/dist/src/commands/ios-scanner.d.ts +27 -0
  21. package/dist/src/commands/ios-scanner.d.ts.map +1 -0
  22. package/dist/src/commands/ios-scanner.js +337 -0
  23. package/dist/src/commands/reset.d.ts +7 -0
  24. package/dist/src/commands/reset.d.ts.map +1 -0
  25. package/dist/src/commands/reset.js +81 -0
  26. package/dist/src/commands/secrets-sync-workflow.d.ts +15 -0
  27. package/dist/src/commands/secrets-sync-workflow.d.ts.map +1 -0
  28. package/dist/src/commands/secrets-sync-workflow.js +255 -0
  29. package/dist/src/commands/secrets-upload.d.ts +21 -0
  30. package/dist/src/commands/secrets-upload.d.ts.map +1 -0
  31. package/dist/src/commands/secrets-upload.js +177 -0
  32. package/dist/src/commands/secrets-upload.test.d.ts +5 -0
  33. package/dist/src/commands/secrets-upload.test.d.ts.map +1 -0
  34. package/dist/src/commands/secrets-upload.test.js +60 -0
  35. package/dist/src/config.d.ts +3 -0
  36. package/dist/src/config.d.ts.map +1 -0
  37. package/dist/src/config.js +46 -0
  38. package/dist/src/envman/cli.d.ts +21 -0
  39. package/dist/src/envman/cli.d.ts.map +1 -0
  40. package/dist/src/envman/cli.js +240 -0
  41. package/dist/src/envman/envman.d.ts +83 -0
  42. package/dist/src/envman/envman.d.ts.map +1 -0
  43. package/dist/src/envman/envman.js +361 -0
  44. package/dist/src/envman/envman.test.d.ts +5 -0
  45. package/dist/src/envman/envman.test.d.ts.map +1 -0
  46. package/dist/src/envman/envman.test.js +236 -0
  47. package/dist/src/envman/index.d.ts +23 -0
  48. package/dist/src/envman/index.d.ts.map +1 -0
  49. package/dist/src/envman/index.js +23 -0
  50. package/dist/src/envman/types.d.ts +55 -0
  51. package/dist/src/envman/types.d.ts.map +1 -0
  52. package/dist/src/envman/types.js +12 -0
  53. package/dist/src/lib.d.ts +27 -0
  54. package/dist/src/lib.d.ts.map +1 -0
  55. package/dist/src/lib.js +32 -0
  56. package/dist/src/pipeline.d.ts +3 -0
  57. package/dist/src/pipeline.d.ts.map +1 -0
  58. package/dist/src/pipeline.js +57 -0
  59. package/dist/src/runner.d.ts +17 -0
  60. package/dist/src/runner.d.ts.map +1 -0
  61. package/dist/src/runner.js +234 -0
  62. package/dist/src/types.d.ts +57 -0
  63. package/dist/src/types.d.ts.map +1 -0
  64. package/dist/src/types.js +2 -0
  65. package/dist/src/yaml/bitrise-compat.d.ts +65 -0
  66. package/dist/src/yaml/bitrise-compat.d.ts.map +1 -0
  67. package/dist/src/yaml/bitrise-compat.js +206 -0
  68. package/dist/src/yaml/bitrise-compat.test.d.ts +5 -0
  69. package/dist/src/yaml/bitrise-compat.test.d.ts.map +1 -0
  70. package/dist/src/yaml/bitrise-compat.test.js +347 -0
  71. package/dist/src/yaml/converter.d.ts +33 -0
  72. package/dist/src/yaml/converter.d.ts.map +1 -0
  73. package/dist/src/yaml/converter.js +222 -0
  74. package/dist/src/yaml/converter.test.d.ts +5 -0
  75. package/dist/src/yaml/converter.test.d.ts.map +1 -0
  76. package/dist/src/yaml/converter.test.js +348 -0
  77. package/dist/src/yaml/e2e.test.d.ts +6 -0
  78. package/dist/src/yaml/e2e.test.d.ts.map +1 -0
  79. package/dist/src/yaml/e2e.test.js +446 -0
  80. package/dist/src/yaml/env-resolver.d.ts +120 -0
  81. package/dist/src/yaml/env-resolver.d.ts.map +1 -0
  82. package/dist/src/yaml/env-resolver.js +405 -0
  83. package/dist/src/yaml/env-resolver.test.d.ts +5 -0
  84. package/dist/src/yaml/env-resolver.test.d.ts.map +1 -0
  85. package/dist/src/yaml/env-resolver.test.js +502 -0
  86. package/dist/src/yaml/interactive-prompts.d.ts +71 -0
  87. package/dist/src/yaml/interactive-prompts.d.ts.map +1 -0
  88. package/dist/src/yaml/interactive-prompts.js +258 -0
  89. package/dist/src/yaml/missing-env-handler.d.ts +45 -0
  90. package/dist/src/yaml/missing-env-handler.d.ts.map +1 -0
  91. package/dist/src/yaml/missing-env-handler.js +64 -0
  92. package/dist/src/yaml/parser.d.ts +33 -0
  93. package/dist/src/yaml/parser.d.ts.map +1 -0
  94. package/dist/src/yaml/parser.js +145 -0
  95. package/dist/src/yaml/pipeline-with-secrets.d.ts +25 -0
  96. package/dist/src/yaml/pipeline-with-secrets.d.ts.map +1 -0
  97. package/dist/src/yaml/pipeline-with-secrets.js +76 -0
  98. package/dist/src/yaml/platform-detector.d.ts +83 -0
  99. package/dist/src/yaml/platform-detector.d.ts.map +1 -0
  100. package/dist/src/yaml/platform-detector.js +188 -0
  101. package/dist/src/yaml/platform-detector.test.d.ts +5 -0
  102. package/dist/src/yaml/platform-detector.test.d.ts.map +1 -0
  103. package/dist/src/yaml/platform-detector.test.js +414 -0
  104. package/dist/src/yaml/preflight-validation.d.ts +40 -0
  105. package/dist/src/yaml/preflight-validation.d.ts.map +1 -0
  106. package/dist/src/yaml/preflight-validation.js +152 -0
  107. package/dist/src/yaml/secrets-manager.d.ts +77 -0
  108. package/dist/src/yaml/secrets-manager.d.ts.map +1 -0
  109. package/dist/src/yaml/secrets-manager.js +219 -0
  110. package/dist/src/yaml/step-validator.d.ts +54 -0
  111. package/dist/src/yaml/step-validator.d.ts.map +1 -0
  112. package/dist/src/yaml/step-validator.js +403 -0
  113. package/dist/src/yaml/steps/android-sign.d.ts +35 -0
  114. package/dist/src/yaml/steps/android-sign.d.ts.map +1 -0
  115. package/dist/src/yaml/steps/android-sign.js +147 -0
  116. package/dist/src/yaml/steps/android-version.d.ts +26 -0
  117. package/dist/src/yaml/steps/android-version.d.ts.map +1 -0
  118. package/dist/src/yaml/steps/android-version.js +128 -0
  119. package/dist/src/yaml/steps/android-version.test.d.ts +5 -0
  120. package/dist/src/yaml/steps/android-version.test.d.ts.map +1 -0
  121. package/dist/src/yaml/steps/android-version.test.js +196 -0
  122. package/dist/src/yaml/steps/android.d.ts +95 -0
  123. package/dist/src/yaml/steps/android.d.ts.map +1 -0
  124. package/dist/src/yaml/steps/android.js +916 -0
  125. package/dist/src/yaml/steps/app-store-deploy.d.ts +48 -0
  126. package/dist/src/yaml/steps/app-store-deploy.d.ts.map +1 -0
  127. package/dist/src/yaml/steps/app-store-deploy.js +162 -0
  128. package/dist/src/yaml/steps/base.d.ts +238 -0
  129. package/dist/src/yaml/steps/base.d.ts.map +1 -0
  130. package/dist/src/yaml/steps/base.js +345 -0
  131. package/dist/src/yaml/steps/bitrise-android-tools.d.ts +26 -0
  132. package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +1 -0
  133. package/dist/src/yaml/steps/bitrise-android-tools.js +198 -0
  134. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +5 -0
  135. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +1 -0
  136. package/dist/src/yaml/steps/bitrise-android-tools.test.js +280 -0
  137. package/dist/src/yaml/steps/bitrise-apk-info.d.ts +22 -0
  138. package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +1 -0
  139. package/dist/src/yaml/steps/bitrise-apk-info.js +144 -0
  140. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +5 -0
  141. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +1 -0
  142. package/dist/src/yaml/steps/bitrise-apk-info.test.js +331 -0
  143. package/dist/src/yaml/steps/bitrise-slack.d.ts +49 -0
  144. package/dist/src/yaml/steps/bitrise-slack.d.ts.map +1 -0
  145. package/dist/src/yaml/steps/bitrise-slack.js +280 -0
  146. package/dist/src/yaml/steps/bitrise-slack.test.d.ts +5 -0
  147. package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +1 -0
  148. package/dist/src/yaml/steps/bitrise-slack.test.js +484 -0
  149. package/dist/src/yaml/steps/bitrise-ssh.d.ts +27 -0
  150. package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +1 -0
  151. package/dist/src/yaml/steps/bitrise-ssh.js +134 -0
  152. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +5 -0
  153. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +1 -0
  154. package/dist/src/yaml/steps/bitrise-ssh.test.js +205 -0
  155. package/dist/src/yaml/steps/cache.d.ts +52 -0
  156. package/dist/src/yaml/steps/cache.d.ts.map +1 -0
  157. package/dist/src/yaml/steps/cache.js +351 -0
  158. package/dist/src/yaml/steps/fastlane.d.ts +27 -0
  159. package/dist/src/yaml/steps/fastlane.d.ts.map +1 -0
  160. package/dist/src/yaml/steps/fastlane.js +79 -0
  161. package/dist/src/yaml/steps/file.d.ts +27 -0
  162. package/dist/src/yaml/steps/file.d.ts.map +1 -0
  163. package/dist/src/yaml/steps/file.js +35 -0
  164. package/dist/src/yaml/steps/flutter.d.ts +63 -0
  165. package/dist/src/yaml/steps/flutter.d.ts.map +1 -0
  166. package/dist/src/yaml/steps/flutter.js +215 -0
  167. package/dist/src/yaml/steps/git-clone.d.ts +26 -0
  168. package/dist/src/yaml/steps/git-clone.d.ts.map +1 -0
  169. package/dist/src/yaml/steps/git-clone.js +111 -0
  170. package/dist/src/yaml/steps/google-play-deploy.d.ts +37 -0
  171. package/dist/src/yaml/steps/google-play-deploy.d.ts.map +1 -0
  172. package/dist/src/yaml/steps/google-play-deploy.js +193 -0
  173. package/dist/src/yaml/steps/google-play-deploy.test.d.ts +5 -0
  174. package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +1 -0
  175. package/dist/src/yaml/steps/google-play-deploy.test.js +310 -0
  176. package/dist/src/yaml/steps/index.d.ts +10 -0
  177. package/dist/src/yaml/steps/index.d.ts.map +1 -0
  178. package/dist/src/yaml/steps/index.js +1361 -0
  179. package/dist/src/yaml/steps/ios-deps.d.ts +43 -0
  180. package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -0
  181. package/dist/src/yaml/steps/ios-deps.js +141 -0
  182. package/dist/src/yaml/steps/ios-deps.test.d.ts +5 -0
  183. package/dist/src/yaml/steps/ios-deps.test.d.ts.map +1 -0
  184. package/dist/src/yaml/steps/ios-deps.test.js +90 -0
  185. package/dist/src/yaml/steps/ios-signing.d.ts +31 -0
  186. package/dist/src/yaml/steps/ios-signing.d.ts.map +1 -0
  187. package/dist/src/yaml/steps/ios-signing.js +144 -0
  188. package/dist/src/yaml/steps/ios-version.d.ts +47 -0
  189. package/dist/src/yaml/steps/ios-version.d.ts.map +1 -0
  190. package/dist/src/yaml/steps/ios-version.js +151 -0
  191. package/dist/src/yaml/steps/linting.d.ts +47 -0
  192. package/dist/src/yaml/steps/linting.d.ts.map +1 -0
  193. package/dist/src/yaml/steps/linting.js +148 -0
  194. package/dist/src/yaml/steps/phase2.test.d.ts +6 -0
  195. package/dist/src/yaml/steps/phase2.test.d.ts.map +1 -0
  196. package/dist/src/yaml/steps/phase2.test.js +197 -0
  197. package/dist/src/yaml/steps/phase3.test.d.ts +5 -0
  198. package/dist/src/yaml/steps/phase3.test.d.ts.map +1 -0
  199. package/dist/src/yaml/steps/phase3.test.js +144 -0
  200. package/dist/src/yaml/steps/phase4.test.d.ts +5 -0
  201. package/dist/src/yaml/steps/phase4.test.d.ts.map +1 -0
  202. package/dist/src/yaml/steps/phase4.test.js +166 -0
  203. package/dist/src/yaml/steps/phase5.test.d.ts +6 -0
  204. package/dist/src/yaml/steps/phase5.test.d.ts.map +1 -0
  205. package/dist/src/yaml/steps/phase5.test.js +263 -0
  206. package/dist/src/yaml/steps/registry.d.ts +88 -0
  207. package/dist/src/yaml/steps/registry.d.ts.map +1 -0
  208. package/dist/src/yaml/steps/registry.js +125 -0
  209. package/dist/src/yaml/steps/registry.test.d.ts +5 -0
  210. package/dist/src/yaml/steps/registry.test.d.ts.map +1 -0
  211. package/dist/src/yaml/steps/registry.test.js +235 -0
  212. package/dist/src/yaml/steps/release.d.ts +50 -0
  213. package/dist/src/yaml/steps/release.d.ts.map +1 -0
  214. package/dist/src/yaml/steps/release.js +154 -0
  215. package/dist/src/yaml/steps/script.d.ts +23 -0
  216. package/dist/src/yaml/steps/script.d.ts.map +1 -0
  217. package/dist/src/yaml/steps/script.js +63 -0
  218. package/dist/src/yaml/steps/spec-validation.test.d.ts +6 -0
  219. package/dist/src/yaml/steps/spec-validation.test.d.ts.map +1 -0
  220. package/dist/src/yaml/steps/spec-validation.test.js +130 -0
  221. package/dist/src/yaml/steps/steps.test.d.ts +6 -0
  222. package/dist/src/yaml/steps/steps.test.d.ts.map +1 -0
  223. package/dist/src/yaml/steps/steps.test.js +474 -0
  224. package/dist/src/yaml/steps/test-config.d.ts +3 -0
  225. package/dist/src/yaml/steps/test-config.d.ts.map +1 -0
  226. package/dist/src/yaml/steps/test-config.js +16 -0
  227. package/dist/src/yaml/steps/xcode-new.test.d.ts +5 -0
  228. package/dist/src/yaml/steps/xcode-new.test.d.ts.map +1 -0
  229. package/dist/src/yaml/steps/xcode-new.test.js +211 -0
  230. package/dist/src/yaml/steps/xcode.d.ts +222 -0
  231. package/dist/src/yaml/steps/xcode.d.ts.map +1 -0
  232. package/dist/src/yaml/steps/xcode.js +999 -0
  233. package/dist/src/yaml/types.d.ts +68 -0
  234. package/dist/src/yaml/types.d.ts.map +1 -0
  235. package/dist/src/yaml/types.js +5 -0
  236. package/dist/src/yaml/validation-types.d.ts +96 -0
  237. package/dist/src/yaml/validation-types.d.ts.map +1 -0
  238. package/dist/src/yaml/validation-types.js +8 -0
  239. package/dist/src/yaml/yaml-updater.d.ts +24 -0
  240. package/dist/src/yaml/yaml-updater.d.ts.map +1 -0
  241. package/dist/src/yaml/yaml-updater.js +128 -0
  242. package/package.json +16 -4
@@ -0,0 +1,999 @@
1
+ /**
2
+ * Xcode step implementations (xcodebuild, xcode-test)
3
+ */
4
+ import { existsSync } from 'node:fs';
5
+ import { BaseStepExecutor } from './base.js';
6
+ /**
7
+ * Xcodebuild step executor
8
+ * Builds iOS/macOS projects using xcodebuild
9
+ */
10
+ export class XcodeBuildStepExecutor extends BaseStepExecutor {
11
+ getValidationRequirements(inputs, _env, _config) {
12
+ const requirements = [];
13
+ // xcodebuild command must exist
14
+ requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS builds', 'Install Xcode from the App Store, then run: xcode-select --install'));
15
+ // Required inputs
16
+ requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
17
+ requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
18
+ // Project file - runtime check only if it doesn't already exist locally
19
+ if (inputs.project_path && !existsSync(inputs.project_path)) {
20
+ requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
21
+ }
22
+ return requirements;
23
+ }
24
+ async execute(inputs, env, config) {
25
+ const stepName = 'xcodebuild';
26
+ // Get project path - required
27
+ let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
28
+ // Resolve path identifiers
29
+ if (projectPath.startsWith('builds:')) {
30
+ projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
31
+ }
32
+ // Get scheme - required
33
+ const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
34
+ // Get configuration - defaults to Release
35
+ const configuration = this.getInput(inputs, 'configuration', 'Release');
36
+ // Get destination - defaults to generic/platform=iOS
37
+ const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS');
38
+ // Get output directory - defaults to build
39
+ const outputDir = this.getInput(inputs, 'output_dir', 'build');
40
+ // Get clean build flag
41
+ const isCleanBuild = this.getInput(inputs, 'is_clean_build', false);
42
+ // Get xcconfig content - optional
43
+ const xcconfigContent = this.getInput(inputs, 'xcconfig_content', '');
44
+ const commands = [];
45
+ commands.push('# Xcode Build');
46
+ commands.push(`echo "Building project: ${this.escapeBash(projectPath)}"`);
47
+ commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
48
+ commands.push(`echo "Configuration: ${this.escapeBash(configuration)}"`);
49
+ // Create output directory and resolve to absolute path
50
+ // Using an absolute path for CONFIGURATION_BUILD_DIR prevents xcodebuild from
51
+ // creating a 'build' directory inside each SPM package checkout in DerivedData,
52
+ // which causes "File exists but is not a directory" errors on repeated runs.
53
+ commands.push('');
54
+ commands.push('# Create output directory and resolve to absolute path');
55
+ commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
56
+ commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
57
+ // Create xcconfig file if content provided
58
+ if (xcconfigContent && xcconfigContent.trim() !== '') {
59
+ commands.push('');
60
+ commands.push('# Create custom xcconfig file');
61
+ commands.push('XCCONFIG_FILE=".ci-custom.xcconfig"');
62
+ commands.push('cat > "$XCCONFIG_FILE" << \'EOF\'');
63
+ commands.push(xcconfigContent);
64
+ commands.push('EOF');
65
+ commands.push('echo "Custom xcconfig created"');
66
+ }
67
+ // Determine if workspace or project
68
+ commands.push('');
69
+ commands.push('# Determine project type');
70
+ const escapedPath = this.escapeBash(projectPath);
71
+ commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
72
+ commands.push(' PROJECT_TYPE="-workspace"');
73
+ commands.push('else');
74
+ commands.push(' PROJECT_TYPE="-project"');
75
+ commands.push('fi');
76
+ // Build xcodebuild command
77
+ commands.push('');
78
+ commands.push('# Build xcodebuild command');
79
+ let xcodebuildCmd = 'xcodebuild';
80
+ // Add clean if requested
81
+ if (isCleanBuild) {
82
+ xcodebuildCmd += ' clean';
83
+ }
84
+ xcodebuildCmd += ' build';
85
+ xcodebuildCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
86
+ xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
87
+ xcodebuildCmd += ` -configuration '${this.escapeBash(configuration)}'`;
88
+ xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
89
+ // Add build path (absolute, resolved above)
90
+ xcodebuildCmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
91
+ // Add xcconfig if provided
92
+ if (xcconfigContent && xcconfigContent.trim() !== '') {
93
+ xcodebuildCmd += ' -xcconfig "$XCCONFIG_FILE"';
94
+ }
95
+ commands.push(xcodebuildCmd);
96
+ // Show build results
97
+ commands.push('');
98
+ commands.push('# Build completed');
99
+ commands.push('echo "Build completed successfully"');
100
+ commands.push('echo "Build artifacts in: $OUTPUT_DIR"');
101
+ commands.push('ls -lh "$OUTPUT_DIR" || true');
102
+ const script = this.createBashScriptFromCommands(commands, stepName);
103
+ return this.createScriptStep(script, stepName);
104
+ }
105
+ }
106
+ /**
107
+ * Xcode test step executor
108
+ * Runs tests for iOS/macOS projects using xcodebuild test
109
+ */
110
+ export class XcodeTestStepExecutor extends BaseStepExecutor {
111
+ getValidationRequirements(inputs, _env, _config) {
112
+ const requirements = [];
113
+ // xcodebuild command must exist
114
+ requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS tests', 'Install Xcode from the App Store, then run: xcode-select --install'));
115
+ // Required inputs
116
+ requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
117
+ requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
118
+ // Project file - runtime check only if it doesn't already exist locally
119
+ if (inputs.project_path && !existsSync(inputs.project_path)) {
120
+ requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
121
+ }
122
+ return requirements;
123
+ }
124
+ async execute(inputs, env, config) {
125
+ const stepName = 'xcode-test';
126
+ // Get project path - required
127
+ let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
128
+ // Resolve path identifiers
129
+ if (projectPath.startsWith('builds:')) {
130
+ projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
131
+ }
132
+ // Get scheme - required
133
+ const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
134
+ // Get destination - defaults to platform=iOS Simulator
135
+ const destination = this.getInput(inputs, 'destination', 'platform=iOS Simulator,name=iPhone 14,OS=latest');
136
+ // Get test plan - optional
137
+ const testPlan = this.getInput(inputs, 'test_plan', '');
138
+ // Get code coverage flag
139
+ const isCodeCoverageEnabled = this.getInput(inputs, 'is_code_coverage_enabled', false);
140
+ const commands = [];
141
+ commands.push('# Xcode Test');
142
+ commands.push(`echo "Testing project: ${this.escapeBash(projectPath)}"`);
143
+ commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
144
+ // Determine if workspace or project
145
+ commands.push('');
146
+ commands.push('# Determine project type');
147
+ const escapedPath = this.escapeBash(projectPath);
148
+ commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
149
+ commands.push(' PROJECT_TYPE="-workspace"');
150
+ commands.push('else');
151
+ commands.push(' PROJECT_TYPE="-project"');
152
+ commands.push('fi');
153
+ // Build xcodebuild test command
154
+ commands.push('');
155
+ commands.push('# Run tests');
156
+ let xcodebuildCmd = 'xcodebuild test';
157
+ xcodebuildCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
158
+ xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
159
+ xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
160
+ // Add test plan if specified
161
+ if (testPlan && testPlan.trim() !== '') {
162
+ xcodebuildCmd += ` -testPlan '${this.escapeBash(testPlan)}'`;
163
+ }
164
+ // Add code coverage if enabled
165
+ if (isCodeCoverageEnabled) {
166
+ xcodebuildCmd += ' -enableCodeCoverage YES';
167
+ }
168
+ commands.push(xcodebuildCmd);
169
+ // Show test results
170
+ commands.push('');
171
+ commands.push('# Tests completed');
172
+ commands.push('echo "Tests completed successfully"');
173
+ const script = this.createBashScriptFromCommands(commands, stepName);
174
+ return this.createScriptStep(script, stepName);
175
+ }
176
+ }
177
+ /**
178
+ * xcode-archive step executor
179
+ * Archives and exports an iOS app to IPA, mirroring bitrise xcode-archive@5.
180
+ */
181
+ export class XcodeArchiveStepExecutor extends BaseStepExecutor {
182
+ getValidationRequirements(inputs, _env, _config) {
183
+ const requirements = [];
184
+ requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS builds', 'Install Xcode from the App Store, then run: xcode-select --install'));
185
+ requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
186
+ requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
187
+ // Project file - runtime check only if it doesn't already exist locally
188
+ if (inputs.project_path && !existsSync(inputs.project_path)) {
189
+ requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
190
+ }
191
+ return requirements;
192
+ }
193
+ getOutputs() {
194
+ return [
195
+ { name: 'CIBUILD_IPA_PATH', type: 'environment', description: 'Path to the exported IPA file' },
196
+ { name: 'CIBUILD_XCARCHIVE_PATH', type: 'environment', description: 'Path to the generated .xcarchive' },
197
+ { name: 'CIBUILD_DSYM_PATH', type: 'environment', description: 'Path to the zipped dSYM files' },
198
+ { name: 'CIBUILD_APP_PATH', type: 'environment', description: 'Path to the .app inside the xcarchive (when code signing is off)' },
199
+ ];
200
+ }
201
+ async execute(inputs, env, config) {
202
+ const stepName = 'xcode-archive';
203
+ let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
204
+ if (projectPath.startsWith('builds:')) {
205
+ projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
206
+ }
207
+ const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
208
+ const configuration = this.getInput(inputs, 'configuration', 'Release');
209
+ const platform = this.getInput(inputs, 'platform', 'iOS');
210
+ const distributionMethod = this.getInput(inputs, 'distribution_method', 'development');
211
+ const performClean = this.getInput(inputs, 'perform_clean_action', 'no');
212
+ const compileBitcode = this.getInput(inputs, 'compile_bitcode', 'yes');
213
+ const uploadBitcode = this.getInput(inputs, 'upload_bitcode', 'yes');
214
+ const automaticCodeSigning = this.getInput(inputs, 'automatic_code_signing', 'off');
215
+ const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
216
+ const xcconfigContent = this.getInput(inputs, 'xcconfig_content', 'COMPILER_INDEX_STORE_ENABLE = NO');
217
+ const exportOptionsPlistContent = this.getInput(inputs, 'export_options_plist_content', '');
218
+ const outputDir = this.getInput(inputs, 'output_dir', 'build/ipa');
219
+ const artifactName = this.getInput(inputs, 'artifact_name', '');
220
+ const escapedPath = this.escapeBash(projectPath);
221
+ const escapedScheme = this.escapeBash(scheme);
222
+ const escapedConfig = this.escapeBash(configuration);
223
+ const commands = [];
224
+ commands.push('# xcode-archive — archive and export iOS app to IPA');
225
+ commands.push(`echo "Project: ${escapedPath}"`);
226
+ commands.push(`echo "Scheme: ${escapedScheme}"`);
227
+ commands.push(`echo "Configuration: ${escapedConfig}"`);
228
+ commands.push(`echo "Distribution method: ${this.escapeBash(distributionMethod)}"`);
229
+ commands.push('');
230
+ // Determine project type
231
+ commands.push('# Determine project type (-workspace vs -project)');
232
+ commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
233
+ commands.push(' PROJECT_TYPE="-workspace"');
234
+ commands.push('else');
235
+ commands.push(' PROJECT_TYPE="-project"');
236
+ commands.push('fi');
237
+ commands.push('');
238
+ // Derive artifact name
239
+ if (artifactName) {
240
+ commands.push(`ARTIFACT_NAME="${this.escapeBash(artifactName)}"`);
241
+ }
242
+ else {
243
+ commands.push(`ARTIFACT_NAME="${escapedScheme}"`);
244
+ }
245
+ // Paths
246
+ commands.push(`OUTPUT_DIR="${this.escapeBash(outputDir)}"`);
247
+ commands.push('ARCHIVE_PATH="$OUTPUT_DIR/archive/${ARTIFACT_NAME}.xcarchive"');
248
+ commands.push('mkdir -p "$(dirname "$ARCHIVE_PATH")"');
249
+ commands.push('mkdir -p "$OUTPUT_DIR"');
250
+ commands.push('');
251
+ // Write xcconfig override
252
+ commands.push('# Write xcconfig overrides');
253
+ commands.push('XCCONFIG_PATH="$(mktemp -t cibuild-xcconfig).xcconfig"');
254
+ // When automatic_code_signing is off (default), disable code signing so
255
+ // archives succeed on CI without a development team / provisioning profile.
256
+ let effectiveXcconfig = xcconfigContent.trim() || 'COMPILER_INDEX_STORE_ENABLE = NO';
257
+ if (automaticCodeSigning === 'off') {
258
+ effectiveXcconfig += '\nCODE_SIGNING_ALLOWED = NO';
259
+ effectiveXcconfig += '\nCODE_SIGN_IDENTITY = -';
260
+ }
261
+ commands.push(`cat > "$XCCONFIG_PATH" << 'XCCONFIG_EOF'`);
262
+ commands.push(effectiveXcconfig);
263
+ commands.push('XCCONFIG_EOF');
264
+ commands.push('');
265
+ // Build xcodebuild archive command
266
+ commands.push('# Run xcodebuild archive');
267
+ let archiveCmd = 'xcodebuild';
268
+ if (performClean === 'yes')
269
+ archiveCmd += ' clean';
270
+ archiveCmd += ' archive';
271
+ archiveCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
272
+ archiveCmd += ` -scheme '${escapedScheme}'`;
273
+ archiveCmd += ` -configuration '${escapedConfig}'`;
274
+ archiveCmd += ` -destination 'generic/platform=${this.escapeBash(platform)}'`;
275
+ archiveCmd += ' -archivePath "$ARCHIVE_PATH"';
276
+ archiveCmd += ' -xcconfig "$XCCONFIG_PATH"';
277
+ // When code signing is off, also pass settings as command-line build
278
+ // settings so they take highest priority and override any target-level
279
+ // signing configuration in the Xcode project.
280
+ if (automaticCodeSigning === 'off') {
281
+ archiveCmd += ' CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO';
282
+ }
283
+ if (xcodebuildOptions) {
284
+ archiveCmd += ` ${xcodebuildOptions}`;
285
+ }
286
+ commands.push(archiveCmd);
287
+ commands.push('');
288
+ commands.push('echo "✓ Archive created: $ARCHIVE_PATH"');
289
+ commands.push('');
290
+ // Write output env var helper (used in both signing and non-signing paths)
291
+ commands.push('# Export output environment variables');
292
+ commands.push('ENVSTORE_FILE="${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"');
293
+ commands.push('export_env() {');
294
+ commands.push(' local key="$1" val="$2"');
295
+ commands.push(' if [ -f "$ENVSTORE_FILE" ]; then');
296
+ commands.push(' node -e "');
297
+ commands.push(' const fs=require(\'fs\');');
298
+ commands.push(' const s=JSON.parse(fs.readFileSync(process.argv[1],\'utf-8\'));');
299
+ commands.push(' s.envs=s.envs||[];');
300
+ commands.push(' s.envs=s.envs.filter(e=>e.key!==process.argv[2]);');
301
+ commands.push(' s.envs.push({key:process.argv[2],value:process.argv[3]});');
302
+ commands.push(' fs.writeFileSync(process.argv[1],JSON.stringify(s));');
303
+ commands.push(' " "$ENVSTORE_FILE" "$key" "$val" 2>/dev/null || true');
304
+ commands.push(' fi');
305
+ commands.push(' export "$key=$val"');
306
+ commands.push(' echo " $key=$val"');
307
+ commands.push('}');
308
+ commands.push('');
309
+ if (automaticCodeSigning === 'off') {
310
+ // Signing is disabled — skip IPA export (unsigned archives cannot be exported).
311
+ // Output the xcarchive path and the .app inside it instead.
312
+ commands.push('# Code signing disabled — export xcarchive path only');
313
+ commands.push('export_env CIBUILD_XCARCHIVE_PATH "$(pwd)/$ARCHIVE_PATH"');
314
+ commands.push('');
315
+ commands.push('# Locate .app inside the xcarchive for downstream use');
316
+ commands.push('APP_PATH=$(find "$ARCHIVE_PATH/Products" -name "*.app" -maxdepth 3 2>/dev/null | head -1 || true)');
317
+ commands.push('[ -n "$APP_PATH" ] && export_env CIBUILD_APP_PATH "$(pwd)/$APP_PATH" || true');
318
+ commands.push('');
319
+ commands.push('echo "✓ xcode-archive complete (unsigned — code signing disabled)"');
320
+ }
321
+ else {
322
+ // Signing enabled — generate ExportOptions.plist and export to IPA
323
+ commands.push('# Generate ExportOptions.plist');
324
+ commands.push('EXPORT_PLIST="$(mktemp -t ExportOptions).plist"');
325
+ if (exportOptionsPlistContent.trim()) {
326
+ commands.push(`cat > "$EXPORT_PLIST" << 'PLIST_EOF'`);
327
+ commands.push(exportOptionsPlistContent);
328
+ commands.push('PLIST_EOF');
329
+ }
330
+ else {
331
+ // Map deprecated "development" method name to "debugging" (Apple renamed it in Xcode 15)
332
+ const exportMethod = distributionMethod === 'development' ? 'debugging' : distributionMethod;
333
+ const compileBitcodeVal = compileBitcode === 'yes' ? 'true' : 'false';
334
+ const uploadBitcodeVal = uploadBitcode === 'yes' ? 'true' : 'false';
335
+ const signingStyle = 'automatic';
336
+ commands.push(`cat > "$EXPORT_PLIST" << 'PLIST_EOF'`);
337
+ commands.push('<?xml version="1.0" encoding="UTF-8"?>');
338
+ commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
339
+ commands.push('<plist version="1.0">');
340
+ commands.push('<dict>');
341
+ commands.push(' <key>method</key>');
342
+ commands.push(` <string>${exportMethod}</string>`);
343
+ commands.push(' <key>signingStyle</key>');
344
+ commands.push(` <string>${signingStyle}</string>`);
345
+ if (exportMethod !== 'app-store') {
346
+ commands.push(' <key>compileBitcode</key>');
347
+ commands.push(` <${compileBitcodeVal}/>`);
348
+ }
349
+ if (exportMethod === 'app-store') {
350
+ commands.push(' <key>uploadBitcode</key>');
351
+ commands.push(` <${uploadBitcodeVal}/>`);
352
+ }
353
+ commands.push('</dict>');
354
+ commands.push('</plist>');
355
+ commands.push('PLIST_EOF');
356
+ }
357
+ commands.push('');
358
+ // Export archive to IPA
359
+ commands.push('# Export archive to IPA');
360
+ let exportCmd = 'xcodebuild -exportArchive';
361
+ exportCmd += ' -archivePath "$ARCHIVE_PATH"';
362
+ exportCmd += ' -exportOptionsPlist "$EXPORT_PLIST"';
363
+ exportCmd += ' -exportPath "$OUTPUT_DIR"';
364
+ commands.push(exportCmd);
365
+ commands.push('');
366
+ // Locate generated IPA
367
+ commands.push('# Locate generated IPA and export paths');
368
+ commands.push('IPA_PATH=$(find "$OUTPUT_DIR" -name "*.ipa" | head -1)');
369
+ commands.push('DSYM_PATH=$(find "$OUTPUT_DIR" -name "*.dSYM.zip" 2>/dev/null | head -1 || true)');
370
+ commands.push('');
371
+ commands.push('if [ -z "$IPA_PATH" ]; then');
372
+ commands.push(' echo "Error: IPA not found in $OUTPUT_DIR" >&2');
373
+ commands.push(' exit 1');
374
+ commands.push('fi');
375
+ commands.push('');
376
+ commands.push('echo "✓ IPA created: $IPA_PATH"');
377
+ commands.push('ls -lh "$IPA_PATH"');
378
+ commands.push('');
379
+ commands.push('export_env CIBUILD_IPA_PATH "$IPA_PATH"');
380
+ commands.push('export_env CIBUILD_XCARCHIVE_PATH "$(pwd)/$ARCHIVE_PATH"');
381
+ commands.push('[ -n "$DSYM_PATH" ] && export_env CIBUILD_DSYM_PATH "$DSYM_PATH" || true');
382
+ commands.push('');
383
+ commands.push('echo "✓ xcode-archive complete"');
384
+ commands.push('rm -f "$EXPORT_PLIST"');
385
+ }
386
+ commands.push('rm -f "$XCCONFIG_PATH"');
387
+ const script = this.createBashScriptFromCommands(commands, stepName);
388
+ return this.createScriptStep(script, stepName);
389
+ }
390
+ }
391
+ /**
392
+ * iOS Archive step executor
393
+ * Creates an IPA file from a .app bundle
394
+ */
395
+ export class IosArchiveStepExecutor extends BaseStepExecutor {
396
+ getValidationRequirements(inputs, _env, _config) {
397
+ const requirements = [];
398
+ // Required input
399
+ requirements.push(this.requireInput('app_path', inputs, 'Path to .app bundle', 'Provide app_path input pointing to your built .app file'));
400
+ // zip command for creating IPA
401
+ requirements.push(this.requireCommand('zip', 'zip utility for creating IPA files', 'zip should be pre-installed on macOS'));
402
+ // .app file - runtime check (created by xcodebuild)
403
+ if (inputs.app_path) {
404
+ requirements.push(this.runtimeRequirement(inputs.app_path, '.app bundle file', 'directory', 'xcodebuild'));
405
+ }
406
+ return requirements;
407
+ }
408
+ async execute(inputs, env, config) {
409
+ const stepName = 'ios-archive';
410
+ // Get app path - required
411
+ let appPath = this.getRequiredInput(inputs, 'app_path', stepName);
412
+ // Resolve path identifiers
413
+ if (appPath.startsWith('builds:')) {
414
+ appPath = appPath.replace('builds:', `${config.paths.buildsDir}/`);
415
+ }
416
+ // Get output path - defaults to artifacts/
417
+ const outputPath = this.getInput(inputs, 'output_path', 'artifacts');
418
+ // Get output name - optional, will derive from .app name if not provided
419
+ const outputName = this.getInput(inputs, 'output_name', '');
420
+ const commands = [];
421
+ commands.push('# iOS Archive - Create IPA');
422
+ commands.push(`echo "Creating IPA from: ${this.escapeBash(appPath)}"`);
423
+ // Check if .app exists
424
+ commands.push('');
425
+ commands.push('# Verify .app bundle exists');
426
+ commands.push(`if [ ! -d '${this.escapeBash(appPath)}' ]; then`);
427
+ commands.push(` echo "Error: .app bundle not found at ${this.escapeBash(appPath)}"`);
428
+ commands.push(' exit 1');
429
+ commands.push('fi');
430
+ // Determine output IPA name
431
+ commands.push('');
432
+ commands.push('# Determine IPA name');
433
+ if (outputName && outputName.trim() !== '') {
434
+ // Use provided name
435
+ const cleanName = outputName.replace(/\.ipa$/, '');
436
+ commands.push(`IPA_NAME="${this.escapeBash(cleanName)}"`);
437
+ }
438
+ else {
439
+ // Derive from .app bundle name
440
+ commands.push(`APP_BUNDLE='${this.escapeBash(appPath)}'`);
441
+ commands.push('IPA_NAME=$(basename "$APP_BUNDLE" .app)');
442
+ }
443
+ commands.push('echo "IPA name: $IPA_NAME.ipa"');
444
+ // Create IPA
445
+ commands.push('');
446
+ commands.push('# Create IPA structure');
447
+ commands.push('TEMP_DIR=$(mktemp -d)');
448
+ commands.push('mkdir -p "$TEMP_DIR/Payload"');
449
+ commands.push('');
450
+ commands.push('# Copy .app to Payload directory');
451
+ commands.push(`cp -r '${this.escapeBash(appPath)}' "$TEMP_DIR/Payload/"`);
452
+ commands.push('');
453
+ commands.push('# Create IPA (zip with .ipa extension)');
454
+ commands.push('cd "$TEMP_DIR"');
455
+ commands.push(`zip -r "$IPA_NAME.ipa" Payload -q`);
456
+ commands.push('cd - > /dev/null');
457
+ commands.push('');
458
+ commands.push('# Create output directory and move IPA');
459
+ commands.push(`mkdir -p '${this.escapeBash(outputPath)}'`);
460
+ commands.push(`mv "$TEMP_DIR/$IPA_NAME.ipa" '${this.escapeBash(outputPath)}/'`);
461
+ commands.push('');
462
+ commands.push('# Cleanup temp directory');
463
+ commands.push('rm -rf "$TEMP_DIR"');
464
+ commands.push('');
465
+ commands.push('# Show result');
466
+ commands.push('echo "✓ IPA created successfully"');
467
+ commands.push(`ls -lh '${this.escapeBash(outputPath)}'/"$IPA_NAME.ipa"`);
468
+ const script = this.createBashScriptFromCommands(commands, stepName);
469
+ return this.createScriptStep(script, stepName);
470
+ }
471
+ }
472
+ /**
473
+ * OTA install step executor.
474
+ * Generates manifest.plist and a QR code for itms-services:// OTA installation.
475
+ * The IPA and manifest must be hosted at user-provided HTTPS URLs.
476
+ */
477
+ export class OtaInstallStepExecutor extends BaseStepExecutor {
478
+ getValidationRequirements(inputs, _env, _config) {
479
+ return [
480
+ this.requireInput('ipa_url', inputs, 'HTTPS URL where the IPA file will be hosted', 'Provide the full HTTPS URL to where the IPA will be uploaded'),
481
+ this.requireInput('bundle_id', inputs, 'iOS bundle identifier (e.g. com.example.app)'),
482
+ this.requireInput('bundle_version', inputs, 'App version string (e.g. 1.2.3)'),
483
+ this.requireInput('title', inputs, 'App display name shown during install'),
484
+ ];
485
+ }
486
+ getOutputs() {
487
+ return [
488
+ {
489
+ name: 'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL',
490
+ type: 'environment',
491
+ description: 'QR code image URL for the OTA install page (api.qrserver.com link)',
492
+ },
493
+ ];
494
+ }
495
+ async execute(inputs, _env, _config) {
496
+ const stepName = 'ota-install';
497
+ const ipaUrl = this.getRequiredInput(inputs, 'ipa_url', stepName);
498
+ const bundleId = this.getRequiredInput(inputs, 'bundle_id', stepName);
499
+ const bundleVersion = this.getRequiredInput(inputs, 'bundle_version', stepName);
500
+ const title = this.getRequiredInput(inputs, 'title', stepName);
501
+ const outputDir = this.getInput(inputs, 'output_dir', '.ci/artifacts');
502
+ const manifestUrlInput = this.getInput(inputs, 'manifest_url', '');
503
+ const escapedIpaUrl = this.escapeBash(ipaUrl);
504
+ const escapedBundleId = this.escapeBash(bundleId);
505
+ const escapedBundleVersion = this.escapeBash(bundleVersion);
506
+ const escapedTitle = this.escapeBash(title);
507
+ const escapedOutputDir = this.escapeBash(outputDir);
508
+ const commands = [];
509
+ commands.push('# ota-install — generate OTA manifest + QR code');
510
+ commands.push(`IPA_URL='${escapedIpaUrl}'`);
511
+ commands.push(`BUNDLE_ID='${escapedBundleId}'`);
512
+ commands.push(`BUNDLE_VERSION='${escapedBundleVersion}'`);
513
+ commands.push(`TITLE='${escapedTitle}'`);
514
+ commands.push(`OUTPUT_DIR='${escapedOutputDir}'`);
515
+ commands.push('');
516
+ // Derive manifest URL if not provided
517
+ if (manifestUrlInput.trim()) {
518
+ commands.push(`MANIFEST_URL='${this.escapeBash(manifestUrlInput)}'`);
519
+ }
520
+ else {
521
+ commands.push('# Derive manifest URL from IPA URL (same directory, manifest.plist filename)');
522
+ commands.push('MANIFEST_URL="$(dirname "$IPA_URL")/manifest.plist"');
523
+ }
524
+ commands.push('');
525
+ // Build OTA link
526
+ commands.push('OTA_URL="itms-services://?action=download-manifest&url=${MANIFEST_URL}"');
527
+ commands.push('');
528
+ // Create output dir and write manifest.plist
529
+ commands.push('mkdir -p "$OUTPUT_DIR"');
530
+ commands.push('');
531
+ commands.push('# Write manifest.plist');
532
+ commands.push('cat > "$OUTPUT_DIR/manifest.plist" << PLIST_EOF');
533
+ commands.push('<?xml version="1.0" encoding="UTF-8"?>');
534
+ commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
535
+ commands.push('<plist version="1.0">');
536
+ commands.push('<dict>');
537
+ commands.push(' <key>items</key>');
538
+ commands.push(' <array>');
539
+ commands.push(' <dict>');
540
+ commands.push(' <key>assets</key>');
541
+ commands.push(' <array>');
542
+ commands.push(' <dict>');
543
+ commands.push(' <key>kind</key><string>software-package</string>');
544
+ commands.push(' <key>url</key><string>${IPA_URL}</string>');
545
+ commands.push(' </dict>');
546
+ commands.push(' </array>');
547
+ commands.push(' <key>metadata</key>');
548
+ commands.push(' <dict>');
549
+ commands.push(' <key>bundle-identifier</key><string>${BUNDLE_ID}</string>');
550
+ commands.push(' <key>bundle-version</key><string>${BUNDLE_VERSION}</string>');
551
+ commands.push(' <key>kind</key><string>software</string>');
552
+ commands.push(' <key>title</key><string>${TITLE}</string>');
553
+ commands.push(' </dict>');
554
+ commands.push(' </dict>');
555
+ commands.push(' </array>');
556
+ commands.push('</dict>');
557
+ commands.push('</plist>');
558
+ commands.push('PLIST_EOF');
559
+ commands.push('');
560
+ commands.push('echo "✓ manifest.plist written to $OUTPUT_DIR/manifest.plist"');
561
+ commands.push('');
562
+ // Generate QR code (qrencode preferred; graceful fallback)
563
+ commands.push('# Generate QR code');
564
+ commands.push('if command -v qrencode &>/dev/null; then');
565
+ commands.push(' echo ""');
566
+ commands.push(' echo "Scan to install:"');
567
+ commands.push(' qrencode -t ansiutf8 "$OTA_URL"');
568
+ commands.push('else');
569
+ commands.push(' echo "ℹ Install qrencode for terminal QR: brew install qrencode"');
570
+ commands.push('fi');
571
+ commands.push('');
572
+ // Print summary
573
+ commands.push('echo ""');
574
+ commands.push('echo "─────────────────────────────────────────"');
575
+ commands.push('echo "OTA Install Summary"');
576
+ commands.push('echo "─────────────────────────────────────────"');
577
+ commands.push('echo "IPA URL: $IPA_URL"');
578
+ commands.push('echo "Manifest URL: $MANIFEST_URL"');
579
+ commands.push('echo "OTA Link: $OTA_URL"');
580
+ commands.push('echo ""');
581
+ commands.push('echo "Host the IPA and manifest.plist at the URLs above, then share the OTA link."');
582
+ commands.push('echo "─────────────────────────────────────────"');
583
+ commands.push('');
584
+ // Export QR image URL to envstore (api.qrserver.com image link, usable in notifications)
585
+ commands.push('# Export QR image URL to envstore for downstream steps (e.g. Slack notifications)');
586
+ commands.push('ENCODED_OTA="$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$OTA_URL" 2>/dev/null || printf "%s" "$OTA_URL" | sed \'s/ /%20/g; s/:/%3A/g; s|/|%2F|g; s/?/%3F/g; s/=/%3D/g; s/&/%26/g\')"');
587
+ commands.push('QR_IMAGE_URL="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${ENCODED_OTA}"');
588
+ commands.push('');
589
+ commands.push('ENVSTORE_FILE="${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"');
590
+ commands.push('if [ -f "$ENVSTORE_FILE" ]; then');
591
+ commands.push(' node -e "');
592
+ commands.push(' const fs=require(\'fs\');');
593
+ commands.push(' const s=JSON.parse(fs.readFileSync(process.argv[1],\'utf-8\'));');
594
+ commands.push(' s.envs=s.envs||[];');
595
+ commands.push(' s.envs=s.envs.filter(e=>e.key!==\'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL\');');
596
+ commands.push(' s.envs.push({key:\'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL\',value:process.argv[2]});');
597
+ commands.push(' fs.writeFileSync(process.argv[1],JSON.stringify(s));');
598
+ commands.push(' " "$ENVSTORE_FILE" "$QR_IMAGE_URL" 2>/dev/null || true');
599
+ commands.push('fi');
600
+ commands.push('export CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL="$QR_IMAGE_URL"');
601
+ const script = this.createBashScriptFromCommands(commands, stepName);
602
+ return this.createScriptStep(script, stepName);
603
+ }
604
+ }
605
+ /**
606
+ * Builds the app and tests without executing them.
607
+ * Produces an .xctestrun file for use with xcode-test-without-building.
608
+ */
609
+ export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
610
+ getValidationRequirements(inputs, _env, _config) {
611
+ const requirements = [];
612
+ requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'));
613
+ requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace'));
614
+ requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name'));
615
+ if (inputs.project_path && !existsSync(inputs.project_path)) {
616
+ requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
617
+ }
618
+ return requirements;
619
+ }
620
+ getOutputs() {
621
+ return [
622
+ { name: 'CIBUILD_TEST_BUNDLE_PATH', type: 'environment', description: 'Directory containing the built test bundle' },
623
+ { name: 'CIBUILD_XCTESTRUN_FILE_PATH', type: 'environment', description: 'Path to the generated .xctestrun file' },
624
+ ];
625
+ }
626
+ async execute(inputs, _env, _config) {
627
+ const stepName = 'xcode-build-for-test';
628
+ const projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
629
+ const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
630
+ const configuration = this.getInput(inputs, 'configuration', 'Debug');
631
+ const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS Simulator');
632
+ const testPlan = this.getInput(inputs, 'test_plan', '');
633
+ const xcconfigContent = this.getInput(inputs, 'xcconfig_content', '');
634
+ const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
635
+ const outputDir = this.getInput(inputs, 'output_dir', 'build');
636
+ const escapedPath = this.escapeBash(projectPath);
637
+ const escapedScheme = this.escapeBash(scheme);
638
+ const commands = [];
639
+ commands.push('# xcode-build-for-test — build-for-testing');
640
+ commands.push(`echo "Building for testing: ${escapedPath}"`);
641
+ commands.push(`echo "Scheme: ${escapedScheme}"`);
642
+ commands.push('');
643
+ // Output directory
644
+ commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
645
+ commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
646
+ commands.push('');
647
+ // xcconfig
648
+ if (xcconfigContent.trim()) {
649
+ commands.push('XCCONFIG_FILE="$(mktemp -t cibuild-xcconfig).xcconfig"');
650
+ commands.push(`cat > "$XCCONFIG_FILE" << 'XCCONFIG_EOF'`);
651
+ commands.push(xcconfigContent);
652
+ commands.push('XCCONFIG_EOF');
653
+ commands.push('');
654
+ }
655
+ // Determine project type
656
+ commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
657
+ commands.push(' PROJECT_TYPE="-workspace"');
658
+ commands.push('else');
659
+ commands.push(' PROJECT_TYPE="-project"');
660
+ commands.push('fi');
661
+ commands.push('');
662
+ // Build command
663
+ let cmd = 'xcodebuild build-for-testing';
664
+ cmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
665
+ cmd += ` -scheme '${escapedScheme}'`;
666
+ cmd += ` -configuration '${this.escapeBash(configuration)}'`;
667
+ cmd += ` -destination '${this.escapeBash(destination)}'`;
668
+ cmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
669
+ if (testPlan) {
670
+ cmd += ` -testPlan '${this.escapeBash(testPlan)}'`;
671
+ }
672
+ if (xcconfigContent.trim()) {
673
+ cmd += ' -xcconfig "$XCCONFIG_FILE"';
674
+ }
675
+ if (xcodebuildOptions) {
676
+ cmd += ` ${xcodebuildOptions}`;
677
+ }
678
+ commands.push(cmd);
679
+ commands.push('');
680
+ // Locate .xctestrun file
681
+ commands.push('# Locate generated .xctestrun file');
682
+ commands.push('XCTESTRUN_PATH=$(find "$OUTPUT_DIR/DerivedData/Build/Products" -name "*.xctestrun" | head -1)');
683
+ commands.push('if [ -z "$XCTESTRUN_PATH" ]; then');
684
+ commands.push(' echo "❌ Error: .xctestrun file not found"');
685
+ commands.push(' exit 1');
686
+ commands.push('fi');
687
+ commands.push('');
688
+ commands.push('TEST_BUNDLE_DIR="$(dirname "$XCTESTRUN_PATH")"');
689
+ commands.push('echo "✅ Build for testing complete"');
690
+ commands.push('echo "xctestrun: $XCTESTRUN_PATH"');
691
+ commands.push('');
692
+ // Export env vars
693
+ commands.push('envman add --key CIBUILD_TEST_BUNDLE_PATH --value "$TEST_BUNDLE_DIR"');
694
+ commands.push('envman add --key CIBUILD_XCTESTRUN_FILE_PATH --value "$XCTESTRUN_PATH"');
695
+ if (xcconfigContent.trim()) {
696
+ commands.push('rm -f "$XCCONFIG_FILE"');
697
+ }
698
+ const script = this.createBashScriptFromCommands(commands, stepName);
699
+ return this.createScriptStep(script, stepName);
700
+ }
701
+ }
702
+ /**
703
+ * Runs pre-compiled tests from an .xctestrun file.
704
+ */
705
+ export class XcodeTestWithoutBuildingStepExecutor extends BaseStepExecutor {
706
+ getValidationRequirements(_inputs, _env, _config) {
707
+ return [
708
+ this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'),
709
+ ];
710
+ }
711
+ getOutputs() {
712
+ return [
713
+ { name: 'CIBUILD_XCRESULT_PATH', type: 'environment', description: 'Path to the .xcresult test result bundle' },
714
+ ];
715
+ }
716
+ async execute(inputs, _env, _config) {
717
+ const stepName = 'xcode-test-without-building';
718
+ const xctestrun = this.getInput(inputs, 'xctestrun', '$CIBUILD_XCTESTRUN_FILE_PATH');
719
+ const destination = this.getInput(inputs, 'destination', 'platform=iOS Simulator,name=iPhone 15,OS=latest');
720
+ const onlyTesting = this.getInput(inputs, 'only_testing', '');
721
+ const skipTesting = this.getInput(inputs, 'skip_testing', '');
722
+ const repetitionMode = this.getInput(inputs, 'test_repetition_mode', 'none');
723
+ const maxRepetitions = this.getInput(inputs, 'maximum_test_repetitions', '3');
724
+ const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
725
+ const commands = [];
726
+ commands.push('# xcode-test-without-building');
727
+ commands.push('echo "Running pre-built tests..."');
728
+ commands.push('');
729
+ // Resolve xctestrun path
730
+ commands.push(`XCTESTRUN_FILE="${xctestrun}"`);
731
+ commands.push('if [ -z "$XCTESTRUN_FILE" ]; then');
732
+ commands.push(' echo "❌ Error: xctestrun file path is empty. Run xcode-build-for-test first."');
733
+ commands.push(' exit 1');
734
+ commands.push('fi');
735
+ commands.push('if [ ! -f "$XCTESTRUN_FILE" ]; then');
736
+ commands.push(' echo "❌ Error: xctestrun file not found: $XCTESTRUN_FILE"');
737
+ commands.push(' exit 1');
738
+ commands.push('fi');
739
+ commands.push('echo "xctestrun: $XCTESTRUN_FILE"');
740
+ commands.push('');
741
+ // Result bundle path
742
+ commands.push('RESULT_BUNDLE_PATH="build/test-results/$(date +%Y%m%d_%H%M%S).xcresult"');
743
+ commands.push('mkdir -p "$(dirname "$RESULT_BUNDLE_PATH")"');
744
+ commands.push('');
745
+ // Build command
746
+ let cmd = 'xcodebuild test-without-building';
747
+ cmd += ' -xctestrun "$XCTESTRUN_FILE"';
748
+ cmd += ` -destination '${this.escapeBash(destination)}'`;
749
+ cmd += ' -resultBundlePath "$RESULT_BUNDLE_PATH"';
750
+ // Test filtering
751
+ if (onlyTesting) {
752
+ for (const test of onlyTesting.split('\n').filter(Boolean)) {
753
+ cmd += ` -only-testing:'${this.escapeBash(test.trim())}'`;
754
+ }
755
+ }
756
+ if (skipTesting) {
757
+ for (const test of skipTesting.split('\n').filter(Boolean)) {
758
+ cmd += ` -skip-testing:'${this.escapeBash(test.trim())}'`;
759
+ }
760
+ }
761
+ // Repetition
762
+ if (repetitionMode && repetitionMode !== 'none') {
763
+ if (repetitionMode === 'until_failure') {
764
+ cmd += ' -run-tests-until-failure';
765
+ }
766
+ else if (repetitionMode === 'retry_on_failure') {
767
+ cmd += ' -retry-tests-on-failure';
768
+ }
769
+ cmd += ` -test-iterations ${maxRepetitions}`;
770
+ }
771
+ if (xcodebuildOptions) {
772
+ cmd += ` ${xcodebuildOptions}`;
773
+ }
774
+ commands.push(cmd);
775
+ commands.push('');
776
+ commands.push('echo "✅ Tests completed"');
777
+ commands.push('echo "Result bundle: $RESULT_BUNDLE_PATH"');
778
+ commands.push('');
779
+ // Export
780
+ commands.push('envman add --key CIBUILD_XCRESULT_PATH --value "$RESULT_BUNDLE_PATH"');
781
+ const script = this.createBashScriptFromCommands(commands, stepName);
782
+ return this.createScriptStep(script, stepName);
783
+ }
784
+ }
785
+ /**
786
+ * Builds an iOS/tvOS/watchOS app for the simulator.
787
+ * Produces a .app bundle with code signing disabled by default.
788
+ */
789
+ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
790
+ getValidationRequirements(inputs, _env, _config) {
791
+ const requirements = [];
792
+ requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'));
793
+ requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace'));
794
+ requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name'));
795
+ if (inputs.project_path && !existsSync(inputs.project_path)) {
796
+ requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
797
+ }
798
+ return requirements;
799
+ }
800
+ getOutputs() {
801
+ return [
802
+ { name: 'CIBUILD_APP_DIR_PATH', type: 'environment', description: 'Path to the generated .app directory' },
803
+ ];
804
+ }
805
+ async execute(inputs, _env, _config) {
806
+ const stepName = 'xcode-build-for-simulator';
807
+ const projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
808
+ const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
809
+ const configuration = this.getInput(inputs, 'configuration', '');
810
+ const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS Simulator');
811
+ const performClean = this.getInput(inputs, 'perform_clean_action', 'no');
812
+ const xcconfigContent = this.getInput(inputs, 'xcconfig_content', 'CODE_SIGNING_ALLOWED=NO');
813
+ const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
814
+ const outputDir = this.getInput(inputs, 'output_dir', 'build');
815
+ const escapedPath = this.escapeBash(projectPath);
816
+ const escapedScheme = this.escapeBash(scheme);
817
+ const commands = [];
818
+ commands.push('# xcode-build-for-simulator');
819
+ commands.push(`echo "Building for simulator: ${escapedPath}"`);
820
+ commands.push(`echo "Scheme: ${escapedScheme}"`);
821
+ commands.push('');
822
+ // Output dir
823
+ commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
824
+ commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
825
+ commands.push('');
826
+ // xcconfig
827
+ commands.push('XCCONFIG_FILE="$(mktemp -t cibuild-xcconfig).xcconfig"');
828
+ if (xcconfigContent.trim()) {
829
+ commands.push(`cat > "$XCCONFIG_FILE" << 'XCCONFIG_EOF'`);
830
+ commands.push(xcconfigContent);
831
+ commands.push('XCCONFIG_EOF');
832
+ }
833
+ else {
834
+ commands.push('echo "CODE_SIGNING_ALLOWED=NO" > "$XCCONFIG_FILE"');
835
+ }
836
+ commands.push('');
837
+ // Project type
838
+ commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
839
+ commands.push(' PROJECT_TYPE="-workspace"');
840
+ commands.push('else');
841
+ commands.push(' PROJECT_TYPE="-project"');
842
+ commands.push('fi');
843
+ commands.push('');
844
+ // Build command
845
+ let cmd = 'xcodebuild';
846
+ if (performClean === 'yes') {
847
+ cmd += ' clean';
848
+ }
849
+ cmd += ' build';
850
+ cmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
851
+ cmd += ` -scheme '${escapedScheme}'`;
852
+ if (configuration) {
853
+ cmd += ` -configuration '${this.escapeBash(configuration)}'`;
854
+ }
855
+ cmd += ` -destination '${this.escapeBash(destination)}'`;
856
+ cmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
857
+ cmd += ' -xcconfig "$XCCONFIG_FILE"';
858
+ if (xcodebuildOptions) {
859
+ cmd += ` ${xcodebuildOptions}`;
860
+ }
861
+ commands.push(cmd);
862
+ commands.push('');
863
+ // Locate .app
864
+ commands.push('# Locate generated .app');
865
+ commands.push('APP_PATH=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.app" -type d | head -1)');
866
+ commands.push('if [ -z "$APP_PATH" ]; then');
867
+ commands.push(' echo "❌ Error: .app not found in $OUTPUT_DIR"');
868
+ commands.push(' exit 1');
869
+ commands.push('fi');
870
+ commands.push('');
871
+ commands.push('echo "✅ Simulator build complete"');
872
+ commands.push('echo "App: $APP_PATH"');
873
+ commands.push('');
874
+ // Export
875
+ commands.push('envman add --key CIBUILD_APP_DIR_PATH --value "$APP_PATH"');
876
+ commands.push('rm -f "$XCCONFIG_FILE"');
877
+ const script = this.createBashScriptFromCommands(commands, stepName);
878
+ return this.createScriptStep(script, stepName);
879
+ }
880
+ }
881
+ /**
882
+ * Exports an IPA from an existing .xcarchive using xcodebuild -exportArchive.
883
+ */
884
+ export class ExportXcarchiveStepExecutor extends BaseStepExecutor {
885
+ getValidationRequirements(_inputs, _env, _config) {
886
+ return [
887
+ this.requireCommand('xcodebuild', 'Xcode is required for archive export'),
888
+ ];
889
+ }
890
+ getOutputs() {
891
+ return [
892
+ { name: 'CIBUILD_IPA_PATH', type: 'environment', description: 'Path to the exported IPA file' },
893
+ { name: 'CIBUILD_DSYM_PATH', type: 'environment', description: 'Path to the zipped dSYM files' },
894
+ ];
895
+ }
896
+ async execute(inputs, _env, _config) {
897
+ const stepName = 'export-xcarchive';
898
+ const archivePath = this.getRequiredInput(inputs, 'archive_path', stepName);
899
+ const product = this.getInput(inputs, 'product', 'app');
900
+ const distributionMethod = this.getInput(inputs, 'distribution_method', 'development');
901
+ const compileBitcode = this.getInput(inputs, 'compile_bitcode', 'yes') === 'yes';
902
+ const uploadBitcode = this.getInput(inputs, 'upload_bitcode', 'yes') === 'yes';
903
+ const exportOptionsPlistContent = this.getInput(inputs, 'export_options_plist_content', '');
904
+ const verbose = this.getInput(inputs, 'verbose_log', 'no') === 'yes';
905
+ const commands = [];
906
+ commands.push('# export-xcarchive — export IPA from .xcarchive');
907
+ commands.push('echo "📦 Exporting IPA from xcarchive..."');
908
+ commands.push('');
909
+ commands.push(`ARCHIVE_PATH='${this.escapeBash(archivePath)}'`);
910
+ commands.push('');
911
+ // Validate archive exists
912
+ commands.push('if [ ! -d "$ARCHIVE_PATH" ]; then');
913
+ commands.push(' echo "❌ Error: Archive not found: $ARCHIVE_PATH"');
914
+ commands.push(' exit 1');
915
+ commands.push('fi');
916
+ commands.push('');
917
+ // Set up export directory
918
+ commands.push('EXPORT_DIR="${CIBUILD_DEPLOY_DIR:-./artifacts}/export"');
919
+ commands.push('mkdir -p "$EXPORT_DIR"');
920
+ commands.push('');
921
+ // Generate or use provided ExportOptions.plist
922
+ commands.push('EXPORT_OPTIONS="$EXPORT_DIR/ExportOptions.plist"');
923
+ if (exportOptionsPlistContent) {
924
+ commands.push('cat > "$EXPORT_OPTIONS" << \'PLIST_EOF\'');
925
+ commands.push(exportOptionsPlistContent);
926
+ commands.push('PLIST_EOF');
927
+ }
928
+ else {
929
+ // Map distribution method for Xcode 15.3+
930
+ commands.push('# Determine distribution method');
931
+ commands.push(`METHOD='${this.escapeBash(distributionMethod)}'`);
932
+ commands.push('XCODE_VERSION=$(xcodebuild -version | head -1 | grep -oE "[0-9]+\\.[0-9]+" || echo "0.0")');
933
+ commands.push('XCODE_MAJOR=$(echo "$XCODE_VERSION" | cut -d. -f1)');
934
+ commands.push('XCODE_MINOR=$(echo "$XCODE_VERSION" | cut -d. -f2)');
935
+ commands.push('if [ "$XCODE_MAJOR" -ge 16 ] || ([ "$XCODE_MAJOR" -eq 15 ] && [ "$XCODE_MINOR" -ge 3 ]); then');
936
+ commands.push(' case "$METHOD" in');
937
+ commands.push(' development) METHOD="debugging" ;;');
938
+ commands.push(' app-store) METHOD="app-store-connect" ;;');
939
+ commands.push(' ad-hoc) METHOD="release-testing" ;;');
940
+ commands.push(' esac');
941
+ commands.push('fi');
942
+ commands.push('');
943
+ commands.push('cat > "$EXPORT_OPTIONS" << PLIST_EOF');
944
+ commands.push('<?xml version="1.0" encoding="UTF-8"?>');
945
+ commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
946
+ commands.push('<plist version="1.0">');
947
+ commands.push('<dict>');
948
+ commands.push(' <key>method</key>');
949
+ commands.push(' <string>$METHOD</string>');
950
+ commands.push(` <key>compileBitcode</key>`);
951
+ commands.push(` <${compileBitcode}/>`);
952
+ commands.push(` <key>uploadBitcode</key>`);
953
+ commands.push(` <${uploadBitcode}/>`);
954
+ if (product === 'app-clip') {
955
+ commands.push(' <key>exportForThinning</key>');
956
+ commands.push(' <string>app-clip</string>');
957
+ }
958
+ commands.push('</dict>');
959
+ commands.push('</plist>');
960
+ commands.push('PLIST_EOF');
961
+ }
962
+ commands.push('');
963
+ // Run xcodebuild -exportArchive
964
+ const verboseFlag = verbose ? '' : ' -quiet';
965
+ commands.push('echo "Exporting with method: $METHOD"');
966
+ commands.push(`xcodebuild -exportArchive \\`);
967
+ commands.push(` -archivePath "$ARCHIVE_PATH" \\`);
968
+ commands.push(` -exportPath "$EXPORT_DIR" \\`);
969
+ commands.push(` -exportOptionsPlist "$EXPORT_OPTIONS"${verboseFlag}`);
970
+ commands.push('');
971
+ // Find IPA
972
+ commands.push('# Find exported IPA');
973
+ commands.push('IPA_PATH=$(find "$EXPORT_DIR" -name "*.ipa" | head -1)');
974
+ commands.push('if [ -z "$IPA_PATH" ]; then');
975
+ commands.push(' echo "❌ Error: No IPA found in export directory"');
976
+ commands.push(' exit 1');
977
+ commands.push('fi');
978
+ commands.push('echo "✅ Exported IPA: $IPA_PATH"');
979
+ commands.push('');
980
+ // Collect dSYMs
981
+ commands.push('# Collect dSYMs');
982
+ commands.push('DSYM_DIR="$ARCHIVE_PATH/dSYMs"');
983
+ commands.push('DSYM_ZIP=""');
984
+ commands.push('if [ -d "$DSYM_DIR" ] && [ "$(ls -A "$DSYM_DIR" 2>/dev/null)" ]; then');
985
+ commands.push(' DSYM_ZIP="$EXPORT_DIR/dSYMs.zip"');
986
+ commands.push(' cd "$DSYM_DIR" && zip -r "$DSYM_ZIP" . && cd -');
987
+ commands.push(' echo "dSYMs: $DSYM_ZIP"');
988
+ commands.push('fi');
989
+ commands.push('');
990
+ // Export env vars
991
+ commands.push('envman add --key CIBUILD_IPA_PATH --value "$IPA_PATH"');
992
+ commands.push('if [ -n "$DSYM_ZIP" ]; then');
993
+ commands.push(' envman add --key CIBUILD_DSYM_PATH --value "$DSYM_ZIP"');
994
+ commands.push('fi');
995
+ const script = this.createBashScriptFromCommands(commands, stepName);
996
+ return this.createScriptStep(script, stepName);
997
+ }
998
+ }
999
+ //# sourceMappingURL=xcode.js.map