@invarn/cibuild 1.3.16 → 1.3.18

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 +47 -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 +58 -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 +352 -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 +505 -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 +17 -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,1096 @@
1
+ import { resolve } from "node:path";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import prompts from "prompts";
4
+ import { scanAndroidProject, formatScanResult } from "./android-scanner.js";
5
+ import { scanIosProject, formatIosScanResult } from "./ios-scanner.js";
6
+ import { SecretsManager } from "../yaml/secrets-manager.js";
7
+ import { collectFileSecret } from "./file-secret-collector.js";
8
+ import { generateGitHubActionsWorkflow } from "./github-workflow.js";
9
+ function variantFromName(name) {
10
+ const buildType = name.charAt(0).toUpperCase() + name.slice(1);
11
+ return { variant: name, buildType, gradleTask: `assemble${buildType}` };
12
+ }
13
+ const DEFAULT_VARIANTS = {
14
+ primary: variantFromName("debug"),
15
+ pullRequest: variantFromName("debug"),
16
+ release: variantFromName("release"),
17
+ };
18
+ async function promptScheme(workflowLabel, defaultScheme, detectedSchemes) {
19
+ const choices = detectedSchemes.map((s) => ({ title: s, value: s }));
20
+ choices.push({ title: "Other (enter custom scheme)", value: "__custom__" });
21
+ const defaultIndex = detectedSchemes.indexOf(defaultScheme);
22
+ const initial = defaultIndex !== -1 ? defaultIndex : 0;
23
+ const { choice } = await prompts({
24
+ type: "select",
25
+ name: "choice",
26
+ message: `Xcode scheme for ${workflowLabel}:`,
27
+ choices,
28
+ initial,
29
+ });
30
+ if (!choice)
31
+ return defaultScheme;
32
+ if (choice === "__custom__") {
33
+ const { name } = await prompts({
34
+ type: "text",
35
+ name: "name",
36
+ message: "Enter scheme name:",
37
+ validate: (v) => (v.trim().length > 0 ? true : "Scheme name is required"),
38
+ });
39
+ return name?.trim() || defaultScheme;
40
+ }
41
+ return choice;
42
+ }
43
+ async function promptConfiguration(workflowLabel, defaultConfig) {
44
+ const choices = [
45
+ { title: "Debug", value: "Debug" },
46
+ { title: "Release", value: "Release" },
47
+ { title: "Other (enter custom configuration)", value: "__custom__" },
48
+ ];
49
+ const defaultIndex = choices.findIndex((c) => c.value === defaultConfig);
50
+ const initial = defaultIndex !== -1 ? defaultIndex : 0;
51
+ const { choice } = await prompts({
52
+ type: "select",
53
+ name: "choice",
54
+ message: `Build configuration for ${workflowLabel}:`,
55
+ choices,
56
+ initial,
57
+ });
58
+ if (!choice)
59
+ return defaultConfig;
60
+ if (choice === "__custom__") {
61
+ const { name } = await prompts({
62
+ type: "text",
63
+ name: "name",
64
+ message: "Enter configuration name (e.g. Staging, AdHoc):",
65
+ validate: (v) => (v.trim().length > 0 ? true : "Configuration name is required"),
66
+ });
67
+ return name?.trim() || defaultConfig;
68
+ }
69
+ return choice;
70
+ }
71
+ async function promptDistributionMethod(workflowLabel, defaultMethod) {
72
+ const choices = [
73
+ { title: "development — development signing, internal use", value: "development" },
74
+ { title: "ad-hoc — Ad Hoc distribution (specific devices)", value: "ad-hoc" },
75
+ { title: "app-store — App Store / TestFlight upload", value: "app-store" },
76
+ { title: "enterprise — Enterprise in-house distribution", value: "enterprise" },
77
+ ];
78
+ const defaultIndex = choices.findIndex((c) => c.value === defaultMethod);
79
+ const initial = defaultIndex !== -1 ? defaultIndex : 0;
80
+ const { choice } = await prompts({
81
+ type: "select",
82
+ name: "choice",
83
+ message: `Distribution method for ${workflowLabel}:`,
84
+ choices,
85
+ initial,
86
+ });
87
+ return choice || defaultMethod;
88
+ }
89
+ async function promptVariant(workflowLabel, defaultName, detectedVariants) {
90
+ const choices = detectedVariants.map((v) => ({ title: v, value: v }));
91
+ choices.push({ title: "Other (enter custom variant)", value: "__custom__" });
92
+ const defaultIndex = detectedVariants.indexOf(defaultName);
93
+ const initial = defaultIndex !== -1 ? defaultIndex : 0;
94
+ const { choice } = await prompts({
95
+ type: "select",
96
+ name: "choice",
97
+ message: `Build variant for ${workflowLabel}:`,
98
+ choices,
99
+ initial,
100
+ });
101
+ if (!choice)
102
+ return variantFromName(defaultName);
103
+ if (choice === "__custom__") {
104
+ const { name } = await prompts({
105
+ type: "text",
106
+ name: "name",
107
+ message: "Enter variant name (e.g. stagingDebug, freeRelease):",
108
+ validate: (v) => (v.trim().length > 0 ? true : "Variant name is required"),
109
+ });
110
+ return variantFromName(name?.trim() || defaultName);
111
+ }
112
+ return variantFromName(choice);
113
+ }
114
+ function detectSetupNeeds(warnings) {
115
+ return {
116
+ keystore: warnings.some((w) => w.category === "missing-file" && /keystore/i.test(w.message)),
117
+ keystoreProperties: warnings.some((w) => w.category === "missing-file" && /keystore\.properties/i.test(w.message)),
118
+ googleServices: warnings.some((w) => w.category === "firebase" ||
119
+ (w.category === "missing-file" && /google-services/i.test(w.message))),
120
+ googlePlayDeploy: false,
121
+ googlePlayPackageName: '',
122
+ artifactType: 'apk',
123
+ };
124
+ }
125
+ function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} }, variants = DEFAULT_VARIANTS) {
126
+ const keystoreStepFor = (wf) => {
127
+ if (!setup.keystore)
128
+ return "";
129
+ const path = setup.keystorePaths[wf] ?? "keystore.jks";
130
+ return `
131
+ - file@1.0.0:
132
+ title: Setup Keystore
133
+ is_skippable: true
134
+ inputs:
135
+ target_path: ${path}
136
+ var_name: KEYSTORE_BASE64
137
+ base64_encoded: true
138
+ `;
139
+ };
140
+ // Add BUILD_TYPE override to workflow envs when it differs from the global default ("Debug")
141
+ const buildTypeEnvOverride = (v) => v.buildType !== "Debug" ? ` - BUILD_TYPE: ${v.buildType}\n` : "";
142
+ // Optional env var stubs (empty defaults — filled via ci secrets add)
143
+ const keystoreEnv = setup.keystore
144
+ ? ` - KEYSTORE_BASE64: "" # Set via: ci secrets add KEYSTORE_BASE64 -w primary\n`
145
+ : "";
146
+ const keystorePropertiesEnv = setup.keystoreProperties
147
+ ? ` - KEYSTORE_PROPERTIES: "" # Release signing — set via: ci secrets add KEYSTORE_PROPERTIES -w release\n`
148
+ : "";
149
+ const googleServicesEnv = setup.googleServices
150
+ ? ` - GOOGLE_SERVICES_JSON: "" # Set via: ci secrets add GOOGLE_SERVICES_JSON\n`
151
+ : "";
152
+ const googlePlayEnv = setup.googlePlayDeploy
153
+ ? ` - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: "" # Set via: ci secrets add GOOGLE_PLAY_SERVICE_ACCOUNT_JSON -w release\n`
154
+ : "";
155
+ const googleServicesStep = setup.googleServices
156
+ ? `
157
+ - file@1.0.0:
158
+ is_skippable: true
159
+ inputs:
160
+ target_path: app/google-services.json
161
+ var_name: GOOGLE_SERVICES_JSON
162
+ `
163
+ : "";
164
+ const googlePlayStep = setup.googlePlayDeploy
165
+ ? `
166
+ - file@1.0.0:
167
+ title: Setup Google Play Service Account
168
+ is_skippable: true
169
+ inputs:
170
+ target_path: .ci/keys/google-play-service-account.json
171
+ var_name: GOOGLE_PLAY_SERVICE_ACCOUNT_JSON
172
+
173
+ - google-play-deploy@1.0.0:
174
+ inputs:
175
+ service_account_json_key_path: .ci/keys/google-play-service-account.json
176
+ package_name: ${setup.googlePlayPackageName}
177
+ artifact_type: ${setup.artifactType}
178
+ track: internal
179
+ `
180
+ : "";
181
+ const keystorePropertiesStep = setup.keystoreProperties
182
+ ? `
183
+ - script@1.0.0:
184
+ title: Setup keystore.properties
185
+ is_skippable: true
186
+ inputs:
187
+ content: |
188
+ if [ -z "\$KEYSTORE_PROPERTIES" ]; then
189
+ echo "KEYSTORE_PROPERTIES not set — skipping keystore.properties setup"
190
+ exit 0
191
+ fi
192
+ printf '%s' "\$KEYSTORE_PROPERTIES" > "\$PROJECT_LOCATION/keystore.properties"
193
+ echo "✓ keystore.properties created"
194
+ `
195
+ : "";
196
+ const { primary: pv, pullRequest: prv, release: rv } = variants;
197
+ // When deploying to Google Play as AAB, override the release gradle task and labels
198
+ const isAab = setup.googlePlayDeploy && setup.artifactType === 'aab';
199
+ const releaseGradleTask = isAab ? `bundle${rv.buildType}` : rv.gradleTask;
200
+ const releaseArtifactLabel = isAab ? 'AAB' : 'APK';
201
+ const releaseArtifactExt = isAab ? '*.aab' : '*.apk';
202
+ return `format_version: '1'
203
+
204
+ meta:
205
+ cibuild.io:
206
+ stack: linux-docker-android-22.04
207
+ machine_type: standard
208
+ platform: android
209
+
210
+ app:
211
+ envs:
212
+ - PROJECT_LOCATION: .
213
+ - MODULE: app
214
+ - BUILD_TYPE: Debug
215
+ - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true
216
+ ${keystoreEnv}${keystorePropertiesEnv}${googleServicesEnv}${googlePlayEnv}
217
+ workflows:
218
+ primary:
219
+ envs:
220
+ - VARIANT: ${pv.variant}
221
+ ${buildTypeEnvOverride(pv)}
222
+ steps:
223
+ ${keystoreStepFor("primary")}${googleServicesStep}
224
+ - set-java-version@1.0.0:
225
+ inputs:
226
+ java_version: '${javaVersion}'
227
+
228
+ - cache-pull@1.0.0:
229
+ inputs:
230
+ cache_key: gradle-\$MODULE
231
+ cache_paths:
232
+ - ~/.gradle/caches
233
+ - ~/.gradle/wrapper
234
+ - .gradle
235
+
236
+ - android-lint@1.0.0:
237
+ is_skippable: true
238
+ inputs:
239
+ project_location: \$PROJECT_LOCATION
240
+ module: \$MODULE
241
+ variant: \$VARIANT
242
+
243
+ - gradle-build@1.0.0:
244
+ title: Build ${pv.buildType} APK
245
+ inputs:
246
+ project_location: \$PROJECT_LOCATION
247
+ build_type: ${pv.buildType}
248
+ gradle_task: ${pv.gradleTask}
249
+ gradle_options: --stacktrace
250
+
251
+ - android-unit-test@1.0.0:
252
+ is_skippable: true
253
+ inputs:
254
+ project_location: \$PROJECT_LOCATION
255
+ module: \$MODULE
256
+ variant: \$VARIANT
257
+
258
+ - cache-push@1.0.0:
259
+ inputs:
260
+ cache_key: gradle-\$MODULE
261
+ cache_paths:
262
+ - ~/.gradle/caches
263
+ - ~/.gradle/wrapper
264
+ - .gradle
265
+
266
+ pull-request:
267
+ envs:
268
+ - VARIANT: ${prv.variant}
269
+ ${buildTypeEnvOverride(prv)}
270
+ steps:
271
+ ${keystoreStepFor("pull-request")}${googleServicesStep}
272
+ - set-java-version@1.0.0:
273
+ inputs:
274
+ java_version: '${javaVersion}'
275
+
276
+ - cache-pull@1.0.0:
277
+ inputs:
278
+ cache_key: gradle-pr-\$MODULE
279
+ cache_paths:
280
+ - ~/.gradle/caches
281
+ - ~/.gradle/wrapper
282
+
283
+ - android-lint@1.0.0:
284
+ is_skippable: true
285
+ inputs:
286
+ project_location: \$PROJECT_LOCATION
287
+ module: \$MODULE
288
+ variant: \$VARIANT
289
+
290
+ - android-unit-test@1.0.0:
291
+ is_skippable: true
292
+ inputs:
293
+ project_location: \$PROJECT_LOCATION
294
+ module: \$MODULE
295
+ variant: \$VARIANT
296
+
297
+ - gradle-build@1.0.0:
298
+ title: Build PR APK
299
+ inputs:
300
+ project_location: \$PROJECT_LOCATION
301
+ build_type: ${prv.buildType}
302
+ gradle_task: ${prv.gradleTask}
303
+
304
+ - cache-push@1.0.0:
305
+ inputs:
306
+ cache_key: gradle-pr-\$MODULE
307
+ cache_paths:
308
+ - ~/.gradle/caches
309
+ - ~/.gradle/wrapper
310
+
311
+ release:
312
+ envs:
313
+ - VARIANT: ${rv.variant}
314
+ ${buildTypeEnvOverride(rv)}
315
+ steps:
316
+ ${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStep}
317
+ - set-java-version@1.0.0:
318
+ inputs:
319
+ java_version: '${javaVersion}'
320
+
321
+ - gradle-build@1.0.0:
322
+ title: Build ${rv.buildType} ${releaseArtifactLabel}
323
+ inputs:
324
+ project_location: \$PROJECT_LOCATION
325
+ build_type: ${rv.buildType}
326
+ gradle_task: ${releaseGradleTask}
327
+ gradle_options: --stacktrace
328
+
329
+ - android-unit-test@1.0.0:
330
+ is_skippable: true
331
+ inputs:
332
+ project_location: \$PROJECT_LOCATION
333
+ module: \$MODULE
334
+ variant: \$VARIANT
335
+ ${googlePlayStep}
336
+ - script@1.0.0:
337
+ title: Build Summary
338
+ inputs:
339
+ content: |
340
+ echo "Build Complete"
341
+ if [ -d ".ci/artifacts" ]; then
342
+ find .ci/artifacts -name "${releaseArtifactExt}" | while read -r artifact; do
343
+ echo "${releaseArtifactLabel}: \$artifact"
344
+ done
345
+ fi
346
+ `;
347
+ }
348
+ function generateIosPipeline(projectPath, setup, variants) {
349
+ const { primary: pv, pullRequest: prv, release: rv } = variants;
350
+ // Global scheme is the primary scheme (used as default across workflows)
351
+ const globalScheme = pv.scheme;
352
+ // Only emit per-workflow scheme env if it differs from global
353
+ const schemeEnvFor = (scheme) => scheme !== globalScheme ? ` - SCHEME: ${scheme}\n` : "";
354
+ // Configuration override env (global default is Debug)
355
+ const configEnvFor = (cfg) => cfg !== "Debug" ? ` - CONFIGURATION: ${cfg}\n` : "";
356
+ // Optional env var stubs
357
+ const certEnv = setup.codeSigning
358
+ ? ` - CERTIFICATE_P12: "" # Set via: ci secrets add CERTIFICATE_P12 -w release\n` +
359
+ ` - CERTIFICATE_PASSWORD: "" # Set via: ci secrets add CERTIFICATE_PASSWORD -w release\n` +
360
+ ` - PROVISIONING_PROFILE: "" # Set via: ci secrets add PROVISIONING_PROFILE -w release\n`
361
+ : "";
362
+ // CocoaPods install step
363
+ const podInstallStep = setup.cocoaPods
364
+ ? `
365
+ - script@1.0.0:
366
+ title: Install CocoaPods
367
+ inputs:
368
+ content: |
369
+ pod install --repo-update
370
+ `
371
+ : "";
372
+ const podInstallStepPR = setup.cocoaPods
373
+ ? `
374
+ - script@1.0.0:
375
+ title: Install CocoaPods
376
+ inputs:
377
+ content: |
378
+ pod install
379
+ `
380
+ : "";
381
+ // Code signing setup step (release only)
382
+ const codeSigningStep = setup.codeSigning
383
+ ? `
384
+ - script@1.0.0:
385
+ title: Setup Code Signing
386
+ is_skippable: true
387
+ inputs:
388
+ content: |
389
+ if [ -z "\$CERTIFICATE_P12" ]; then
390
+ echo "CERTIFICATE_P12 not set — skipping code signing setup"
391
+ exit 0
392
+ fi
393
+ echo "\$CERTIFICATE_P12" | base64 --decode > /tmp/certificate.p12
394
+ security create-keychain -p "" ci-build.keychain
395
+ security import /tmp/certificate.p12 -k ci-build.keychain -P "\$CERTIFICATE_PASSWORD" -A -T /usr/bin/codesign
396
+ security list-keychains -s ci-build.keychain
397
+ security default-keychain -s ci-build.keychain
398
+ security unlock-keychain -p "" ci-build.keychain
399
+ security set-keychain-settings ci-build.keychain
400
+ if [ -n "\$PROVISIONING_PROFILE" ]; then
401
+ mkdir -p ~/Library/MobileDevice/Provisioning\\ Profiles
402
+ echo "\$PROVISIONING_PROFILE" | base64 --decode > ~/Library/MobileDevice/Provisioning\\ Profiles/profile.mobileprovision
403
+ echo "✓ Provisioning profile installed"
404
+ fi
405
+ rm -f /tmp/certificate.p12
406
+ echo "✓ Code signing setup complete"
407
+ `
408
+ : "";
409
+ // App Store Connect deploy step (release only)
410
+ const appStoreDeployStep = setup.appStoreDeploy
411
+ ? `
412
+ - app-store-deploy@1.0.0:
413
+ inputs:
414
+ skip_metadata: 'yes'
415
+ skip_screenshots: 'yes'
416
+ submit_for_review: 'no'
417
+ `
418
+ : "";
419
+ // Cache paths depend on whether CocoaPods is used
420
+ const cachePaths = setup.cocoaPods
421
+ ? ` - Pods\n - Podfile.lock`
422
+ : ` - .build`;
423
+ return `format_version: '1'
424
+
425
+ meta:
426
+ cibuild.io:
427
+ stack: macos-xcode-26.4
428
+ machine_type: performance
429
+ platform: ios
430
+
431
+ app:
432
+ envs:
433
+ - PROJECT_PATH: ${projectPath}
434
+ - SCHEME: ${globalScheme}
435
+ - CONFIGURATION: Debug
436
+ ${certEnv}
437
+ workflows:
438
+ primary:
439
+ envs:
440
+ ${schemeEnvFor(pv.scheme)}${configEnvFor(pv.configuration)}
441
+ steps:
442
+ - cache-pull@1.0.0:
443
+ inputs:
444
+ cache_key: pods-\$SCHEME
445
+ cache_paths:
446
+ ${cachePaths}
447
+ ${podInstallStep}
448
+ - xcode-archive@1.0.0:
449
+ inputs:
450
+ project_path: \$PROJECT_PATH
451
+ scheme: \$SCHEME
452
+ configuration: \$CONFIGURATION
453
+ distribution_method: ${pv.distributionMethod}
454
+ output_dir: .ci/artifacts
455
+
456
+ - xcode-test@1.0.0:
457
+ is_skippable: true
458
+ inputs:
459
+ project_path: \$PROJECT_PATH
460
+ scheme: \$SCHEME
461
+ destination: platform=iOS Simulator,OS=latest
462
+ is_code_coverage_enabled: true
463
+
464
+ - cache-push@1.0.0:
465
+ inputs:
466
+ cache_key: pods-\$SCHEME
467
+ cache_paths:
468
+ ${cachePaths}
469
+
470
+ pull-request:
471
+ envs:
472
+ ${schemeEnvFor(prv.scheme)}${configEnvFor(prv.configuration)}
473
+ steps:
474
+ - cache-pull@1.0.0:
475
+ inputs:
476
+ cache_key: pods-pr-\$SCHEME
477
+ cache_paths:
478
+ ${cachePaths}
479
+ ${podInstallStepPR}
480
+ - xcode-test@1.0.0:
481
+ is_skippable: true
482
+ inputs:
483
+ project_path: \$PROJECT_PATH
484
+ scheme: \$SCHEME
485
+ destination: platform=iOS Simulator,OS=latest
486
+
487
+ - xcodebuild@1.0.0:
488
+ inputs:
489
+ project_path: \$PROJECT_PATH
490
+ scheme: \$SCHEME
491
+ configuration: \$CONFIGURATION
492
+ destination: generic/platform=iOS Simulator
493
+
494
+ - cache-push@1.0.0:
495
+ inputs:
496
+ cache_key: pods-pr-\$SCHEME
497
+ cache_paths:
498
+ ${cachePaths}
499
+
500
+ release:
501
+ envs:
502
+ - CONFIGURATION: Release
503
+ ${schemeEnvFor(rv.scheme)}
504
+ steps:
505
+ ${codeSigningStep}
506
+ - cache-pull@1.0.0:
507
+ inputs:
508
+ cache_key: pods-\$SCHEME
509
+ cache_paths:
510
+ ${cachePaths}
511
+ ${podInstallStep}
512
+ - xcode-archive@1.0.0:
513
+ inputs:
514
+ project_path: \$PROJECT_PATH
515
+ scheme: \$SCHEME
516
+ configuration: \$CONFIGURATION
517
+ distribution_method: ${rv.distributionMethod}
518
+ perform_clean_action: 'yes'
519
+ compile_bitcode: 'no'
520
+ upload_bitcode: 'no'
521
+ output_dir: .ci/artifacts
522
+ ${appStoreDeployStep}
523
+ - script@1.0.0:
524
+ title: Build Summary
525
+ inputs:
526
+ content: |
527
+ echo "Build Complete"
528
+ if [ -n "\$CIBUILD_IPA_PATH" ]; then
529
+ echo "IPA: \$CIBUILD_IPA_PATH"
530
+ fi
531
+ if [ -n "\$CIBUILD_XCARCHIVE_PATH" ]; then
532
+ echo "Archive: \$CIBUILD_XCARCHIVE_PATH"
533
+ fi
534
+ `;
535
+ }
536
+ /**
537
+ * Prints a hint listing env vars / Gradle properties detected during scanning
538
+ * that haven't been handled by the setup flow (keystore, google-services, etc.).
539
+ * Prompts the user to add them as secrets before running the pipeline.
540
+ */
541
+ function showMissingSecretsHint(scanResult, setupOptions) {
542
+ // Build a set of var names already configured by the setup flow
543
+ const alreadyHandled = new Set();
544
+ if (setupOptions.keystore)
545
+ alreadyHandled.add("KEYSTORE_BASE64");
546
+ if (setupOptions.keystoreProperties)
547
+ alreadyHandled.add("KEYSTORE_PROPERTIES");
548
+ if (setupOptions.googleServices)
549
+ alreadyHandled.add("GOOGLE_SERVICES_JSON");
550
+ // Only show actionable warnings that have a "ci secrets add VAR" hint
551
+ const ACTIONABLE = new Set(["build-config", "env-var", "signing-config", "gradle-property", "secrets-plugin"]);
552
+ const remaining = scanResult.warnings.filter((w) => {
553
+ if (!ACTIONABLE.has(w.category) || w.severity !== "warning")
554
+ return false;
555
+ const varName = w.hint?.match(/ci secrets add (\S+)/)?.[1];
556
+ return varName && !alreadyHandled.has(varName);
557
+ });
558
+ if (remaining.length === 0)
559
+ return;
560
+ const LABELS = {
561
+ "build-config": "BuildConfig env vars",
562
+ "env-var": "Environment variables",
563
+ "signing-config": "Signing credentials",
564
+ "gradle-property": "Gradle properties",
565
+ "secrets-plugin": "Google Secrets Gradle Plugin (local.properties keys)",
566
+ };
567
+ // Group by category — secrets-plugin keys are grouped separately and shown with a file hint
568
+ const byCategory = new Map();
569
+ for (const w of remaining) {
570
+ if (!byCategory.has(w.category))
571
+ byCategory.set(w.category, []);
572
+ byCategory.get(w.category).push(w);
573
+ }
574
+ console.log("\n──────────────────────────────────────────────────");
575
+ console.log("⚠ Additional secrets detected in your Gradle files");
576
+ console.log("──────────────────────────────────────────────────");
577
+ console.log(" These variables were found but not yet configured.\n");
578
+ for (const [cat, items] of byCategory) {
579
+ console.log(` ${LABELS[cat] ?? cat}:`);
580
+ if (cat === "secrets-plugin") {
581
+ // These all go into a single properties file — show as a block with a file-step hint
582
+ const propertiesFile = items[0]?.location ?? "local.properties";
583
+ console.log(` → These keys are read from '${propertiesFile}' by the Secrets Gradle Plugin.`);
584
+ console.log(` → The file is gitignored — inject it as a file step before building.\n`);
585
+ console.log(` Suggested step (add via: ci edit → Add step → file):`);
586
+ console.log(` target_path: ${propertiesFile}`);
587
+ console.log(` var_name: <SECRET_NAME_HOLDING_FILE_CONTENT>\n`);
588
+ console.log(` Keys needed in '${propertiesFile}':`);
589
+ for (const w of items) {
590
+ const key = w.hint?.match(/ci secrets add (\S+)/)?.[1] ?? w.message;
591
+ console.log(` ${key}`);
592
+ }
593
+ }
594
+ else {
595
+ for (const w of items) {
596
+ const varName = w.hint?.match(/ci secrets add (\S+)/)?.[1] ?? w.message;
597
+ const loc = w.location ? ` (${w.location})` : "";
598
+ console.log(` ci secrets add ${varName}${loc}`);
599
+ }
600
+ }
601
+ console.log("");
602
+ }
603
+ }
604
+ async function handleIosBuildCommand(cwd, options = {}) {
605
+ // 1. Scan the project
606
+ console.log("\n🔍 Scanning iOS project for potential unknowns...\n");
607
+ const iosScanResult = await scanIosProject(cwd);
608
+ console.log(formatIosScanResult(iosScanResult));
609
+ // 2. Determine available schemes
610
+ const detectedSchemes = iosScanResult.detectedSchemes.length > 0
611
+ ? iosScanResult.detectedSchemes
612
+ : ["MyApp"];
613
+ if (iosScanResult.detectedSchemes.length > 1) {
614
+ console.log(`ℹ Detected schemes: ${iosScanResult.detectedSchemes.join(", ")}\n`);
615
+ }
616
+ // 3. Prompt for scheme + configuration + distribution method per workflow
617
+ const ni = options.nonInteractive;
618
+ if (!ni)
619
+ console.log("⚙ Configure build schemes and configurations\n");
620
+ const primaryScheme = ni ? detectedSchemes[0] : await promptScheme("primary", detectedSchemes[0], detectedSchemes);
621
+ const primaryConfig = ni ? "Debug" : await promptConfiguration("primary", "Debug");
622
+ const primaryMethod = ni ? "development" : await promptDistributionMethod("primary", "development");
623
+ const pullRequestScheme = ni ? detectedSchemes[0] : await promptScheme("pull-request", primaryScheme, detectedSchemes);
624
+ const pullRequestConfig = ni ? "Debug" : await promptConfiguration("pull-request", "Debug");
625
+ const pullRequestMethod = ni ? "development" : await promptDistributionMethod("pull-request", "development");
626
+ const releaseScheme = ni ? detectedSchemes[0] : await promptScheme("release", primaryScheme, detectedSchemes);
627
+ const releaseConfig = ni ? "Release" : await promptConfiguration("release", "Release");
628
+ const releaseMethod = ni ? "app-store" : await promptDistributionMethod("release", "app-store");
629
+ const iosVariants = {
630
+ primary: { scheme: primaryScheme, configuration: primaryConfig, distributionMethod: primaryMethod },
631
+ pullRequest: { scheme: pullRequestScheme, configuration: pullRequestConfig, distributionMethod: pullRequestMethod },
632
+ release: { scheme: releaseScheme, configuration: releaseConfig, distributionMethod: releaseMethod },
633
+ };
634
+ // 4. Setup options
635
+ const iosSetupOptions = {
636
+ cocoaPods: false,
637
+ codeSigning: false,
638
+ appStoreDeploy: false,
639
+ bundleId: "",
640
+ };
641
+ const secretsManager = new SecretsManager();
642
+ // CocoaPods
643
+ if (iosScanResult.hasCocoaPods) {
644
+ if (ni) {
645
+ iosSetupOptions.cocoaPods = true;
646
+ }
647
+ else {
648
+ const { create } = await prompts({
649
+ type: "confirm",
650
+ name: "create",
651
+ message: "Generate a pod install step for CocoaPods dependencies?",
652
+ initial: true,
653
+ });
654
+ iosSetupOptions.cocoaPods = !!create;
655
+ }
656
+ }
657
+ // Code signing
658
+ if (iosScanResult.hasSigningConfig) {
659
+ if (ni) {
660
+ iosSetupOptions.codeSigning = true;
661
+ }
662
+ else {
663
+ const { create } = await prompts({
664
+ type: "confirm",
665
+ name: "create",
666
+ message: "Generate a code signing setup step for the release workflow?",
667
+ initial: true,
668
+ });
669
+ iosSetupOptions.codeSigning = !!create;
670
+ }
671
+ if (iosSetupOptions.codeSigning) {
672
+ await collectFileSecret({
673
+ displayName: "certificate (.p12)",
674
+ secretName: "CERTIFICATE_P12",
675
+ fileMatch: (name) => name.endsWith(".p12"),
676
+ readAs: "base64",
677
+ workflowGroups: [{ label: "release", workflows: ["release"] }],
678
+ }, cwd, secretsManager, ni);
679
+ if (ni) {
680
+ console.log(" Add later: ci secrets add CERTIFICATE_PASSWORD -w release");
681
+ }
682
+ else {
683
+ const { password } = await prompts({
684
+ type: "password",
685
+ name: "password",
686
+ message: "Certificate password (leave blank to add later):",
687
+ });
688
+ if (password?.trim()) {
689
+ secretsManager.storeSecret("CERTIFICATE_PASSWORD", password.trim(), "release");
690
+ console.log(" ✅ CERTIFICATE_PASSWORD stored for workflow: release");
691
+ }
692
+ else {
693
+ console.log(" Add later: ci secrets add CERTIFICATE_PASSWORD -w release");
694
+ }
695
+ }
696
+ await collectFileSecret({
697
+ displayName: "provisioning profile (.mobileprovision)",
698
+ secretName: "PROVISIONING_PROFILE",
699
+ fileMatch: (name) => name.endsWith(".mobileprovision"),
700
+ readAs: "base64",
701
+ workflowGroups: [{ label: "release", workflows: ["release"] }],
702
+ }, cwd, secretsManager, ni);
703
+ }
704
+ }
705
+ // App Store Connect deploy — skip in non-interactive mode (requires credentials)
706
+ if (!ni) {
707
+ const { deployToAppStore } = await prompts({
708
+ type: "confirm",
709
+ name: "deployToAppStore",
710
+ message: "Add App Store Connect / TestFlight upload step to release workflow?",
711
+ initial: false,
712
+ });
713
+ iosSetupOptions.appStoreDeploy = !!deployToAppStore;
714
+ if (iosSetupOptions.appStoreDeploy) {
715
+ await collectFileSecret({
716
+ displayName: "App Store Connect API key (.p8)",
717
+ secretName: "APPLE_API_KEY_PATH",
718
+ fileMatch: (name) => name.endsWith(".p8"),
719
+ readAs: "base64",
720
+ workflowGroups: [{ label: "release", workflows: ["release"] }],
721
+ }, cwd, secretsManager);
722
+ const { keyId } = await prompts({
723
+ type: "text",
724
+ name: "keyId",
725
+ message: "Apple API Key ID (leave blank to add later):",
726
+ });
727
+ if (keyId?.trim()) {
728
+ secretsManager.storeSecret("APPLE_API_KEY_ID", keyId.trim(), "release");
729
+ console.log(" ✅ APPLE_API_KEY_ID stored for workflow: release");
730
+ }
731
+ else {
732
+ console.log(" Add later: ci secrets add APPLE_API_KEY_ID -w release");
733
+ }
734
+ const { issuerId } = await prompts({
735
+ type: "text",
736
+ name: "issuerId",
737
+ message: "Apple API Issuer ID (leave blank to add later):",
738
+ });
739
+ if (issuerId?.trim()) {
740
+ secretsManager.storeSecret("APPLE_API_ISSUER_ID", issuerId.trim(), "release");
741
+ console.log(" ✅ APPLE_API_ISSUER_ID stored for workflow: release");
742
+ }
743
+ else {
744
+ console.log(" Add later: ci secrets add APPLE_API_ISSUER_ID -w release");
745
+ }
746
+ const detectedTeamId = iosScanResult.developmentTeam;
747
+ let teamIdToStore = detectedTeamId;
748
+ if (detectedTeamId) {
749
+ console.log(` ℹ Detected Apple Team ID from project: ${detectedTeamId}`);
750
+ secretsManager.storeSecret("APPLE_TEAM_ID", detectedTeamId, "release");
751
+ console.log(" ✅ APPLE_TEAM_ID stored for workflow: release");
752
+ }
753
+ else {
754
+ const { teamId } = await prompts({
755
+ type: "text",
756
+ name: "teamId",
757
+ message: "Apple Team ID (leave blank to add later):",
758
+ });
759
+ teamIdToStore = teamId?.trim() || "";
760
+ if (teamIdToStore) {
761
+ secretsManager.storeSecret("APPLE_TEAM_ID", teamIdToStore, "release");
762
+ console.log(" ✅ APPLE_TEAM_ID stored for workflow: release");
763
+ }
764
+ else {
765
+ console.log(" Add later: ci secrets add APPLE_TEAM_ID -w release");
766
+ }
767
+ }
768
+ }
769
+ }
770
+ // 5. Ensure .ci/pipelines/ exists
771
+ const pipelinesDir = resolve(cwd, ".ci", "pipelines");
772
+ if (!existsSync(pipelinesDir)) {
773
+ if (options.createPipelinesDir) {
774
+ mkdirSync(pipelinesDir, { recursive: true });
775
+ console.log("✅ Created .ci/pipelines/");
776
+ }
777
+ else {
778
+ console.error("Project not initialised. Run 'ci init' first.");
779
+ process.exit(1);
780
+ }
781
+ }
782
+ // 6. Check if cibuild.yml already exists
783
+ const outputPath = resolve(pipelinesDir, "cibuild.yml");
784
+ if (existsSync(outputPath) && !ni) {
785
+ const { overwrite } = await prompts({
786
+ type: "confirm",
787
+ name: "overwrite",
788
+ message: ".ci/pipelines/cibuild.yml already exists. Overwrite?",
789
+ initial: false,
790
+ });
791
+ if (!overwrite) {
792
+ console.log("\nCancelled. Existing pipeline was not modified.");
793
+ process.exit(0);
794
+ }
795
+ }
796
+ // 7. Generate and write the pipeline
797
+ const yaml = generateIosPipeline(iosScanResult.projectPath, iosSetupOptions, iosVariants);
798
+ writeFileSync(outputPath, yaml, "utf-8");
799
+ console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
800
+ console.log(" Platform: iOS");
801
+ console.log(` Workflows: primary (${iosVariants.primary.scheme}/${iosVariants.primary.configuration}), pull-request (${iosVariants.pullRequest.scheme}/${iosVariants.pullRequest.configuration}), release (${iosVariants.release.scheme}/${iosVariants.release.configuration})`);
802
+ if (iosSetupOptions.cocoaPods || iosSetupOptions.codeSigning || iosSetupOptions.appStoreDeploy) {
803
+ console.log(" Setup steps included:");
804
+ if (iosSetupOptions.cocoaPods)
805
+ console.log(" • Install CocoaPods (pod install)");
806
+ if (iosSetupOptions.codeSigning)
807
+ console.log(" • Setup Code Signing (CERTIFICATE_P12, PROVISIONING_PROFILE) — release only");
808
+ if (iosSetupOptions.appStoreDeploy)
809
+ console.log(" • App Store Connect / TestFlight upload (app-store-deploy) — release only");
810
+ }
811
+ // Generate GitHub Actions workflow
812
+ let githubWorkflowGenerated = false;
813
+ if (options.createPipelinesDir) {
814
+ githubWorkflowGenerated = generateGitHubActionsWorkflow({ platform: "ios", cwd });
815
+ }
816
+ // Show env var secrets hint
817
+ const envVarWarnings = iosScanResult.warnings.filter((w) => w.category === "env-var" && w.severity === "warning");
818
+ if (envVarWarnings.length > 0) {
819
+ console.log("\n──────────────────────────────────────────────────");
820
+ console.log("⚠ Additional secrets detected in your xcconfig files");
821
+ console.log("──────────────────────────────────────────────────");
822
+ console.log(" These variables were found but not yet configured.\n");
823
+ console.log(" Environment variables:");
824
+ for (const w of envVarWarnings) {
825
+ const varName = w.hint?.match(/ci secrets add (\S+)/)?.[1] ?? w.message;
826
+ const loc = w.location ? ` (${w.location})` : "";
827
+ console.log(` ci secrets add ${varName}${loc}`);
828
+ }
829
+ console.log("");
830
+ }
831
+ const hasSecrets = existsSync(resolve(cwd, ".cibuild-secrets.json"));
832
+ const hasWorkflow = existsSync(resolve(cwd, ".github", "workflows", "ci.yml"));
833
+ console.log("\nNext steps:");
834
+ console.log(" ci run .ci/pipelines/cibuild.yml -w primary # Run locally");
835
+ if (hasWorkflow) {
836
+ if (hasSecrets) {
837
+ console.log(" ci secrets upload # Upload secrets to GitHub");
838
+ }
839
+ console.log(" git add . && git push # Push to GitHub - CI runs automatically");
840
+ }
841
+ console.log("");
842
+ }
843
+ export async function handleBuildCommand(detectMobileProjectRoot, options = {}) {
844
+ const cwd = process.cwd();
845
+ // 1. Detect project type
846
+ const projectType = detectMobileProjectRoot(cwd);
847
+ if (projectType === "ios") {
848
+ await handleIosBuildCommand(cwd, options);
849
+ return;
850
+ }
851
+ if (projectType !== "android") {
852
+ console.error("Error: Not a mobile project root.");
853
+ console.error("'ci build' must be run from the root folder of an Android or iOS project.");
854
+ console.error("\nAndroid projects must contain one of:");
855
+ console.error(" build.gradle, build.gradle.kts, settings.gradle, settings.gradle.kts, gradlew");
856
+ console.error("\niOS projects must contain one of:");
857
+ console.error(" *.xcodeproj, *.xcworkspace, Podfile");
858
+ process.exit(1);
859
+ }
860
+ // 2. Scan the project for potential unknowns
861
+ console.log("\n🔍 Scanning Android project for potential unknowns...\n");
862
+ const scanResult = await scanAndroidProject(cwd);
863
+ console.log(formatScanResult(scanResult));
864
+ // 3. Prompt for build variants per workflow
865
+ const ni = options.nonInteractive;
866
+ const { variants: detectedVariants, productFlavors } = scanResult.buildVariants;
867
+ if (productFlavors.length > 0) {
868
+ console.log(`ℹ Detected product flavors: ${productFlavors.join(", ")}`);
869
+ console.log(` Available variants: ${detectedVariants.join(", ")}\n`);
870
+ }
871
+ let variants;
872
+ if (ni) {
873
+ variants = DEFAULT_VARIANTS;
874
+ }
875
+ else {
876
+ console.log("⚙ Configure build variants\n");
877
+ const primaryVariant = await promptVariant("primary", "debug", detectedVariants);
878
+ const pullRequestVariant = await promptVariant("pull-request", "debug", detectedVariants);
879
+ const releaseVariant = await promptVariant("release", "release", detectedVariants);
880
+ variants = {
881
+ primary: primaryVariant,
882
+ pullRequest: pullRequestVariant,
883
+ release: releaseVariant,
884
+ };
885
+ }
886
+ // 4. Prompt to generate setup steps for detected file issues
887
+ const setupNeeds = detectSetupNeeds(scanResult.warnings);
888
+ const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} };
889
+ const secretsManager = new SecretsManager();
890
+ const keystoreWorkflowGroups = [
891
+ { label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
892
+ { label: "release", workflows: ["release"] },
893
+ ];
894
+ if (setupNeeds.keystore) {
895
+ if (ni) {
896
+ setupOptions.keystore = true;
897
+ }
898
+ else {
899
+ const { create } = await prompts({
900
+ type: "confirm",
901
+ name: "create",
902
+ message: "Generate a setup script step for the keystore?",
903
+ initial: true,
904
+ });
905
+ setupOptions.keystore = !!create;
906
+ }
907
+ if (setupOptions.keystore) {
908
+ setupOptions.keystorePaths = await collectFileSecret({
909
+ displayName: "keystore",
910
+ secretName: "KEYSTORE_BASE64",
911
+ fileMatch: (name) => name.endsWith(".jks") || name.endsWith(".keystore"),
912
+ readAs: "base64",
913
+ workflowGroups: keystoreWorkflowGroups,
914
+ promptTargetPath: true,
915
+ }, cwd, secretsManager, ni);
916
+ }
917
+ }
918
+ if (setupNeeds.keystoreProperties) {
919
+ if (ni) {
920
+ setupOptions.keystoreProperties = true;
921
+ }
922
+ else {
923
+ const { create } = await prompts({
924
+ type: "confirm",
925
+ name: "create",
926
+ message: "Generate a setup script step for keystore.properties (release signing)?",
927
+ initial: true,
928
+ });
929
+ setupOptions.keystoreProperties = !!create;
930
+ }
931
+ if (setupOptions.keystoreProperties) {
932
+ await collectFileSecret({
933
+ displayName: "keystore.properties",
934
+ secretName: "KEYSTORE_PROPERTIES",
935
+ fileMatch: (name) => name === "keystore.properties",
936
+ readAs: "text",
937
+ workflowGroups: [
938
+ { label: "release", workflows: ["release"] },
939
+ ],
940
+ }, cwd, secretsManager, ni);
941
+ }
942
+ }
943
+ // Resolve missing keystore target paths for all configured workflows.
944
+ // For each workflow with no file-selected path: extract storeFile from any
945
+ // KEYSTORE_PROPERTIES secret, then fall back to Gradle-detected paths.
946
+ if (setupOptions.keystore) {
947
+ const gradleKeystorePaths = scanResult.warnings
948
+ .filter((w) => w.category === "missing-file" && w.message.startsWith("Keystore file not found:"))
949
+ .map((w) => w.message.replace("Keystore file not found: ", ""));
950
+ for (const wf of keystoreWorkflowGroups.flatMap((g) => g.workflows)) {
951
+ if (setupOptions.keystorePaths[wf])
952
+ continue;
953
+ const kpContent = secretsManager.getSecret("KEYSTORE_PROPERTIES", wf);
954
+ if (kpContent) {
955
+ const line = kpContent.split("\n").find((l) => l.trim().startsWith("storeFile="));
956
+ if (line) {
957
+ setupOptions.keystorePaths[wf] = line.split("=").slice(1).join("=").trim();
958
+ continue;
959
+ }
960
+ }
961
+ if (gradleKeystorePaths.length > 0) {
962
+ setupOptions.keystorePaths[wf] = gradleKeystorePaths[0];
963
+ }
964
+ }
965
+ }
966
+ if (setupNeeds.googleServices) {
967
+ if (ni) {
968
+ setupOptions.googleServices = true;
969
+ }
970
+ else {
971
+ const { create } = await prompts({
972
+ type: "confirm",
973
+ name: "create",
974
+ message: "Generate a setup script step for google-services.json?",
975
+ initial: true,
976
+ });
977
+ setupOptions.googleServices = !!create;
978
+ }
979
+ if (setupOptions.googleServices) {
980
+ await collectFileSecret({
981
+ displayName: "google-services.json",
982
+ secretName: "GOOGLE_SERVICES_JSON",
983
+ fileMatch: (name) => name === "google-services.json",
984
+ readAs: "text",
985
+ workflowGroups: [
986
+ { label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
987
+ { label: "release", workflows: ["release"] },
988
+ ],
989
+ }, cwd, secretsManager, ni);
990
+ }
991
+ }
992
+ // 4b. Prompt for Google Play deployment setup — skip in non-interactive mode (requires package name + credentials)
993
+ if (!ni) {
994
+ const { setupGooglePlay } = await prompts({
995
+ type: "confirm",
996
+ name: "setupGooglePlay",
997
+ message: "Add a Google Play deploy step to the release workflow?",
998
+ initial: false,
999
+ });
1000
+ if (setupGooglePlay) {
1001
+ const { packageName } = await prompts({
1002
+ type: "text",
1003
+ name: "packageName",
1004
+ message: "Android package name (e.g. com.example.app):",
1005
+ validate: (v) => (v.trim().length > 0 ? true : "Package name is required"),
1006
+ });
1007
+ setupOptions.googlePlayDeploy = true;
1008
+ setupOptions.googlePlayPackageName = packageName?.trim() || '';
1009
+ const { artifactType } = await prompts({
1010
+ type: "select",
1011
+ name: "artifactType",
1012
+ message: "Artifact format for Google Play upload:",
1013
+ choices: [
1014
+ { title: "AAB — Android App Bundle (recommended by Google Play)", value: "aab" },
1015
+ { title: "APK — traditional APK format", value: "apk" },
1016
+ ],
1017
+ initial: 0,
1018
+ });
1019
+ setupOptions.artifactType = artifactType || 'aab';
1020
+ await collectFileSecret({
1021
+ displayName: "Google Play service account JSON",
1022
+ secretName: "GOOGLE_PLAY_SERVICE_ACCOUNT_JSON",
1023
+ fileMatch: (name) => name.endsWith(".json"),
1024
+ readAs: "text",
1025
+ workflowGroups: [
1026
+ { label: "release", workflows: ["release"] },
1027
+ ],
1028
+ }, cwd, secretsManager);
1029
+ }
1030
+ }
1031
+ // 5. Ensure .ci/pipelines/ exists
1032
+ const pipelinesDir = resolve(cwd, ".ci", "pipelines");
1033
+ if (!existsSync(pipelinesDir)) {
1034
+ if (options.createPipelinesDir) {
1035
+ mkdirSync(pipelinesDir, { recursive: true });
1036
+ console.log("✅ Created .ci/pipelines/");
1037
+ }
1038
+ else {
1039
+ console.error("Project not initialised. Run 'ci init' first.");
1040
+ process.exit(1);
1041
+ }
1042
+ }
1043
+ // 6. Check if cibuild.yml already exists
1044
+ const outputPath = resolve(pipelinesDir, "cibuild.yml");
1045
+ if (existsSync(outputPath) && !ni) {
1046
+ const { overwrite } = await prompts({
1047
+ type: "confirm",
1048
+ name: "overwrite",
1049
+ message: ".ci/pipelines/cibuild.yml already exists. Overwrite?",
1050
+ initial: false,
1051
+ });
1052
+ if (!overwrite) {
1053
+ console.log("\nCancelled. Existing pipeline was not modified.");
1054
+ process.exit(0);
1055
+ }
1056
+ }
1057
+ // 7. Generate and write the pipeline
1058
+ // Source/target compatibility (e.g. VERSION_11) doesn't mean the build needs
1059
+ // that exact JDK — a newer JDK can cross-compile. Enforce a minimum of 17
1060
+ // since ubuntu-latest runners ship with JDK 17+ and JDK <17 is unavailable.
1061
+ const javaVersion = Math.max(scanResult.detectedJavaVersion ?? 17, 17);
1062
+ const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants);
1063
+ writeFileSync(outputPath, yaml, "utf-8");
1064
+ console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
1065
+ console.log(" Platform: Android");
1066
+ console.log(` Workflows: primary (${variants.primary.variant}), pull-request (${variants.pullRequest.variant}), release (${variants.release.variant})`);
1067
+ if (setupOptions.keystore || setupOptions.keystoreProperties || setupOptions.googleServices || setupOptions.googlePlayDeploy) {
1068
+ console.log(" Setup steps included:");
1069
+ if (setupOptions.keystore)
1070
+ console.log(" • Setup Keystore (KEYSTORE_BASE64)");
1071
+ if (setupOptions.keystoreProperties)
1072
+ console.log(" • Setup keystore.properties (KEYSTORE_PROPERTIES) — release only");
1073
+ if (setupOptions.googleServices)
1074
+ console.log(" • Setup google-services.json (GOOGLE_SERVICES_JSON)");
1075
+ if (setupOptions.googlePlayDeploy)
1076
+ console.log(` • Google Play deploy (${setupOptions.googlePlayPackageName}) — release only`);
1077
+ }
1078
+ showMissingSecretsHint(scanResult, setupOptions);
1079
+ // Generate GitHub Actions workflow
1080
+ let githubWorkflowGenerated = false;
1081
+ if (options.createPipelinesDir) {
1082
+ githubWorkflowGenerated = generateGitHubActionsWorkflow({ platform: "android", cwd });
1083
+ }
1084
+ const hasSecrets = existsSync(resolve(cwd, ".cibuild-secrets.json"));
1085
+ const hasWorkflow = existsSync(resolve(cwd, ".github", "workflows", "ci.yml"));
1086
+ console.log("\nNext steps:");
1087
+ console.log(" ci run .ci/pipelines/cibuild.yml -w primary # Run locally");
1088
+ if (hasWorkflow) {
1089
+ if (hasSecrets) {
1090
+ console.log(" ci secrets upload # Upload secrets to GitHub");
1091
+ }
1092
+ console.log(" git add . && git push # Push to GitHub - CI runs automatically");
1093
+ }
1094
+ console.log("");
1095
+ }
1096
+ //# sourceMappingURL=build.js.map