@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.
- package/dist/cli.cjs +1 -1
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +987 -0
- package/dist/src/commands/android-scanner.d.ts +32 -0
- package/dist/src/commands/android-scanner.d.ts.map +1 -0
- package/dist/src/commands/android-scanner.js +667 -0
- package/dist/src/commands/build.d.ts +5 -0
- package/dist/src/commands/build.d.ts.map +1 -0
- package/dist/src/commands/build.js +1096 -0
- package/dist/src/commands/edit.d.ts +3 -0
- package/dist/src/commands/edit.d.ts.map +1 -0
- package/dist/src/commands/edit.js +651 -0
- package/dist/src/commands/file-secret-collector.d.ts +37 -0
- package/dist/src/commands/file-secret-collector.d.ts.map +1 -0
- package/dist/src/commands/file-secret-collector.js +199 -0
- package/dist/src/commands/github-workflow.d.ts +5 -0
- package/dist/src/commands/github-workflow.d.ts.map +1 -0
- package/dist/src/commands/github-workflow.js +45 -0
- package/dist/src/commands/ios-scanner.d.ts +27 -0
- package/dist/src/commands/ios-scanner.d.ts.map +1 -0
- package/dist/src/commands/ios-scanner.js +337 -0
- package/dist/src/commands/reset.d.ts +7 -0
- package/dist/src/commands/reset.d.ts.map +1 -0
- package/dist/src/commands/reset.js +81 -0
- package/dist/src/commands/secrets-sync-workflow.d.ts +15 -0
- package/dist/src/commands/secrets-sync-workflow.d.ts.map +1 -0
- package/dist/src/commands/secrets-sync-workflow.js +255 -0
- package/dist/src/commands/secrets-upload.d.ts +21 -0
- package/dist/src/commands/secrets-upload.d.ts.map +1 -0
- package/dist/src/commands/secrets-upload.js +177 -0
- package/dist/src/commands/secrets-upload.test.d.ts +5 -0
- package/dist/src/commands/secrets-upload.test.d.ts.map +1 -0
- package/dist/src/commands/secrets-upload.test.js +60 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +47 -0
- package/dist/src/envman/cli.d.ts +21 -0
- package/dist/src/envman/cli.d.ts.map +1 -0
- package/dist/src/envman/cli.js +240 -0
- package/dist/src/envman/envman.d.ts +83 -0
- package/dist/src/envman/envman.d.ts.map +1 -0
- package/dist/src/envman/envman.js +361 -0
- package/dist/src/envman/envman.test.d.ts +5 -0
- package/dist/src/envman/envman.test.d.ts.map +1 -0
- package/dist/src/envman/envman.test.js +236 -0
- package/dist/src/envman/index.d.ts +23 -0
- package/dist/src/envman/index.d.ts.map +1 -0
- package/dist/src/envman/index.js +23 -0
- package/dist/src/envman/types.d.ts +55 -0
- package/dist/src/envman/types.d.ts.map +1 -0
- package/dist/src/envman/types.js +12 -0
- package/dist/src/lib.d.ts +27 -0
- package/dist/src/lib.d.ts.map +1 -0
- package/dist/src/lib.js +32 -0
- package/dist/src/pipeline.d.ts +3 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +57 -0
- package/dist/src/runner.d.ts +17 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +234 -0
- package/dist/src/types.d.ts +58 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/yaml/bitrise-compat.d.ts +65 -0
- package/dist/src/yaml/bitrise-compat.d.ts.map +1 -0
- package/dist/src/yaml/bitrise-compat.js +206 -0
- package/dist/src/yaml/bitrise-compat.test.d.ts +5 -0
- package/dist/src/yaml/bitrise-compat.test.d.ts.map +1 -0
- package/dist/src/yaml/bitrise-compat.test.js +347 -0
- package/dist/src/yaml/converter.d.ts +33 -0
- package/dist/src/yaml/converter.d.ts.map +1 -0
- package/dist/src/yaml/converter.js +222 -0
- package/dist/src/yaml/converter.test.d.ts +5 -0
- package/dist/src/yaml/converter.test.d.ts.map +1 -0
- package/dist/src/yaml/converter.test.js +348 -0
- package/dist/src/yaml/e2e.test.d.ts +6 -0
- package/dist/src/yaml/e2e.test.d.ts.map +1 -0
- package/dist/src/yaml/e2e.test.js +446 -0
- package/dist/src/yaml/env-resolver.d.ts +120 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -0
- package/dist/src/yaml/env-resolver.js +405 -0
- package/dist/src/yaml/env-resolver.test.d.ts +5 -0
- package/dist/src/yaml/env-resolver.test.d.ts.map +1 -0
- package/dist/src/yaml/env-resolver.test.js +502 -0
- package/dist/src/yaml/interactive-prompts.d.ts +71 -0
- package/dist/src/yaml/interactive-prompts.d.ts.map +1 -0
- package/dist/src/yaml/interactive-prompts.js +258 -0
- package/dist/src/yaml/missing-env-handler.d.ts +45 -0
- package/dist/src/yaml/missing-env-handler.d.ts.map +1 -0
- package/dist/src/yaml/missing-env-handler.js +64 -0
- package/dist/src/yaml/parser.d.ts +33 -0
- package/dist/src/yaml/parser.d.ts.map +1 -0
- package/dist/src/yaml/parser.js +145 -0
- package/dist/src/yaml/pipeline-with-secrets.d.ts +25 -0
- package/dist/src/yaml/pipeline-with-secrets.d.ts.map +1 -0
- package/dist/src/yaml/pipeline-with-secrets.js +76 -0
- package/dist/src/yaml/platform-detector.d.ts +83 -0
- package/dist/src/yaml/platform-detector.d.ts.map +1 -0
- package/dist/src/yaml/platform-detector.js +188 -0
- package/dist/src/yaml/platform-detector.test.d.ts +5 -0
- package/dist/src/yaml/platform-detector.test.d.ts.map +1 -0
- package/dist/src/yaml/platform-detector.test.js +414 -0
- package/dist/src/yaml/preflight-validation.d.ts +40 -0
- package/dist/src/yaml/preflight-validation.d.ts.map +1 -0
- package/dist/src/yaml/preflight-validation.js +152 -0
- package/dist/src/yaml/secrets-manager.d.ts +77 -0
- package/dist/src/yaml/secrets-manager.d.ts.map +1 -0
- package/dist/src/yaml/secrets-manager.js +219 -0
- package/dist/src/yaml/step-validator.d.ts +54 -0
- package/dist/src/yaml/step-validator.d.ts.map +1 -0
- package/dist/src/yaml/step-validator.js +403 -0
- package/dist/src/yaml/steps/android-sign.d.ts +35 -0
- package/dist/src/yaml/steps/android-sign.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-sign.js +147 -0
- package/dist/src/yaml/steps/android-version.d.ts +26 -0
- package/dist/src/yaml/steps/android-version.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-version.js +128 -0
- package/dist/src/yaml/steps/android-version.test.d.ts +5 -0
- package/dist/src/yaml/steps/android-version.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-version.test.js +196 -0
- package/dist/src/yaml/steps/android.d.ts +95 -0
- package/dist/src/yaml/steps/android.d.ts.map +1 -0
- package/dist/src/yaml/steps/android.js +916 -0
- package/dist/src/yaml/steps/app-store-deploy.d.ts +48 -0
- package/dist/src/yaml/steps/app-store-deploy.d.ts.map +1 -0
- package/dist/src/yaml/steps/app-store-deploy.js +162 -0
- package/dist/src/yaml/steps/base.d.ts +238 -0
- package/dist/src/yaml/steps/base.d.ts.map +1 -0
- package/dist/src/yaml/steps/base.js +345 -0
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts +26 -0
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-android-tools.js +198 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.js +280 -0
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts +22 -0
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-apk-info.js +144 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.js +331 -0
- package/dist/src/yaml/steps/bitrise-slack.d.ts +49 -0
- package/dist/src/yaml/steps/bitrise-slack.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-slack.js +280 -0
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-slack.test.js +484 -0
- package/dist/src/yaml/steps/bitrise-ssh.d.ts +27 -0
- package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-ssh.js +134 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.js +205 -0
- package/dist/src/yaml/steps/cache.d.ts +52 -0
- package/dist/src/yaml/steps/cache.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache.js +352 -0
- package/dist/src/yaml/steps/fastlane.d.ts +27 -0
- package/dist/src/yaml/steps/fastlane.d.ts.map +1 -0
- package/dist/src/yaml/steps/fastlane.js +79 -0
- package/dist/src/yaml/steps/file.d.ts +27 -0
- package/dist/src/yaml/steps/file.d.ts.map +1 -0
- package/dist/src/yaml/steps/file.js +35 -0
- package/dist/src/yaml/steps/flutter.d.ts +63 -0
- package/dist/src/yaml/steps/flutter.d.ts.map +1 -0
- package/dist/src/yaml/steps/flutter.js +215 -0
- package/dist/src/yaml/steps/git-clone.d.ts +26 -0
- package/dist/src/yaml/steps/git-clone.d.ts.map +1 -0
- package/dist/src/yaml/steps/git-clone.js +111 -0
- package/dist/src/yaml/steps/google-play-deploy.d.ts +37 -0
- package/dist/src/yaml/steps/google-play-deploy.d.ts.map +1 -0
- package/dist/src/yaml/steps/google-play-deploy.js +193 -0
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts +5 -0
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/google-play-deploy.test.js +310 -0
- package/dist/src/yaml/steps/index.d.ts +10 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -0
- package/dist/src/yaml/steps/index.js +1361 -0
- package/dist/src/yaml/steps/ios-deps.d.ts +43 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-deps.js +141 -0
- package/dist/src/yaml/steps/ios-deps.test.d.ts +5 -0
- package/dist/src/yaml/steps/ios-deps.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-deps.test.js +90 -0
- package/dist/src/yaml/steps/ios-signing.d.ts +31 -0
- package/dist/src/yaml/steps/ios-signing.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-signing.js +144 -0
- package/dist/src/yaml/steps/ios-version.d.ts +47 -0
- package/dist/src/yaml/steps/ios-version.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-version.js +151 -0
- package/dist/src/yaml/steps/linting.d.ts +47 -0
- package/dist/src/yaml/steps/linting.d.ts.map +1 -0
- package/dist/src/yaml/steps/linting.js +148 -0
- package/dist/src/yaml/steps/phase2.test.d.ts +6 -0
- package/dist/src/yaml/steps/phase2.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase2.test.js +197 -0
- package/dist/src/yaml/steps/phase3.test.d.ts +5 -0
- package/dist/src/yaml/steps/phase3.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase3.test.js +144 -0
- package/dist/src/yaml/steps/phase4.test.d.ts +5 -0
- package/dist/src/yaml/steps/phase4.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase4.test.js +166 -0
- package/dist/src/yaml/steps/phase5.test.d.ts +6 -0
- package/dist/src/yaml/steps/phase5.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase5.test.js +263 -0
- package/dist/src/yaml/steps/registry.d.ts +88 -0
- package/dist/src/yaml/steps/registry.d.ts.map +1 -0
- package/dist/src/yaml/steps/registry.js +125 -0
- package/dist/src/yaml/steps/registry.test.d.ts +5 -0
- package/dist/src/yaml/steps/registry.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/registry.test.js +235 -0
- package/dist/src/yaml/steps/release.d.ts +50 -0
- package/dist/src/yaml/steps/release.d.ts.map +1 -0
- package/dist/src/yaml/steps/release.js +154 -0
- package/dist/src/yaml/steps/script.d.ts +23 -0
- package/dist/src/yaml/steps/script.d.ts.map +1 -0
- package/dist/src/yaml/steps/script.js +63 -0
- package/dist/src/yaml/steps/spec-validation.test.d.ts +6 -0
- package/dist/src/yaml/steps/spec-validation.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/spec-validation.test.js +130 -0
- package/dist/src/yaml/steps/steps.test.d.ts +6 -0
- package/dist/src/yaml/steps/steps.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/steps.test.js +505 -0
- package/dist/src/yaml/steps/test-config.d.ts +3 -0
- package/dist/src/yaml/steps/test-config.d.ts.map +1 -0
- package/dist/src/yaml/steps/test-config.js +17 -0
- package/dist/src/yaml/steps/xcode-new.test.d.ts +5 -0
- package/dist/src/yaml/steps/xcode-new.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-new.test.js +211 -0
- package/dist/src/yaml/steps/xcode.d.ts +222 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode.js +999 -0
- package/dist/src/yaml/types.d.ts +68 -0
- package/dist/src/yaml/types.d.ts.map +1 -0
- package/dist/src/yaml/types.js +5 -0
- package/dist/src/yaml/validation-types.d.ts +96 -0
- package/dist/src/yaml/validation-types.d.ts.map +1 -0
- package/dist/src/yaml/validation-types.js +8 -0
- package/dist/src/yaml/yaml-updater.d.ts +24 -0
- package/dist/src/yaml/yaml-updater.d.ts.map +1 -0
- package/dist/src/yaml/yaml-updater.js +128 -0
- 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
|