@invarn/cibuild 1.3.16 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +46 -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 +57 -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 +351 -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 +474 -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 +16 -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,667 @@
|
|
|
1
|
+
import { resolve, relative } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// File discovery
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Returns the list of gradle files to scan.
|
|
8
|
+
* Always includes the root build.gradle and the app/ module.
|
|
9
|
+
* Also parses settings.gradle to find any additional include'd modules.
|
|
10
|
+
*/
|
|
11
|
+
function findGradleFiles(root) {
|
|
12
|
+
const candidates = [];
|
|
13
|
+
// Root-level build files
|
|
14
|
+
for (const name of ["build.gradle", "build.gradle.kts"]) {
|
|
15
|
+
const p = resolve(root, name);
|
|
16
|
+
if (existsSync(p))
|
|
17
|
+
candidates.push(p);
|
|
18
|
+
}
|
|
19
|
+
// Collect module directories from settings.gradle
|
|
20
|
+
const moduleDirs = new Set(["app"]); // always include app/
|
|
21
|
+
for (const name of ["settings.gradle", "settings.gradle.kts"]) {
|
|
22
|
+
const p = resolve(root, name);
|
|
23
|
+
if (!existsSync(p))
|
|
24
|
+
continue;
|
|
25
|
+
const content = readFileSync(p, "utf-8");
|
|
26
|
+
// Matches: include ':module', include(":module"), include ':a', ':b'
|
|
27
|
+
const includeRe = /include\s*[\(']?\s*['"]:([^'")\s,]+)/g;
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = includeRe.exec(content)) !== null) {
|
|
30
|
+
moduleDirs.add(m[1].replace(/:/g, "/"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const mod of moduleDirs) {
|
|
34
|
+
for (const name of ["build.gradle", "build.gradle.kts"]) {
|
|
35
|
+
const p = resolve(root, mod, name);
|
|
36
|
+
if (existsSync(p))
|
|
37
|
+
candidates.push(p);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return candidates;
|
|
41
|
+
}
|
|
42
|
+
function safeRead(path) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(path, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function relPath(root, filePath) {
|
|
51
|
+
return relative(root, filePath);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns line numbers for each match of a regex in file content.
|
|
55
|
+
*/
|
|
56
|
+
function lineNumber(content, index) {
|
|
57
|
+
return content.slice(0, index).split("\n").length;
|
|
58
|
+
}
|
|
59
|
+
/** Extract all System.getenv("VAR") references. */
|
|
60
|
+
function extractEnvVarRefs(content) {
|
|
61
|
+
const results = [];
|
|
62
|
+
const re = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(content)) !== null) {
|
|
65
|
+
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
/** Extract Groovy findProperty("prop") and Kotlin properties["prop"] references. */
|
|
70
|
+
function extractPropertyRefs(content) {
|
|
71
|
+
const results = [];
|
|
72
|
+
// Groovy: project.findProperty("name") or findProperty("name")
|
|
73
|
+
const groovyRe = /(?:project\.)?findProperty\(\s*["']([^"']+)["']\s*\)/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = groovyRe.exec(content)) !== null) {
|
|
76
|
+
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
77
|
+
}
|
|
78
|
+
// Kotlin DSL: properties["name"] or project.properties["name"]
|
|
79
|
+
const kotlinRe = /(?:project\.)?properties\["([^"]+)"\]/g;
|
|
80
|
+
while ((m = kotlinRe.exec(content)) !== null) {
|
|
81
|
+
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract env var names from buildConfigField entries.
|
|
87
|
+
* e.g. buildConfigField "String", "API_KEY", System.getenv("MAPS_API_KEY") ?: ""
|
|
88
|
+
* Returns the System.getenv variable names embedded inside buildConfigField values.
|
|
89
|
+
*/
|
|
90
|
+
function extractBuildConfigEnvRefs(content) {
|
|
91
|
+
const results = [];
|
|
92
|
+
// Match buildConfigField lines that also contain System.getenv
|
|
93
|
+
const lineRe = /buildConfigField\s*[\("]/g;
|
|
94
|
+
let m;
|
|
95
|
+
const envRe = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
96
|
+
while ((m = lineRe.exec(content)) !== null) {
|
|
97
|
+
// Find the end of the statement (next newline or semicolon)
|
|
98
|
+
const lineEnd = content.indexOf("\n", m.index);
|
|
99
|
+
const slice = content.slice(m.index, lineEnd === -1 ? undefined : lineEnd + 1);
|
|
100
|
+
let em;
|
|
101
|
+
while ((em = envRe.exec(slice)) !== null) {
|
|
102
|
+
results.push({ name: em[1], line: lineNumber(content, m.index) });
|
|
103
|
+
}
|
|
104
|
+
envRe.lastIndex = 0;
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns true if the Google Secrets Gradle Plugin is applied.
|
|
110
|
+
* This plugin reads local.properties (or a configured file) and auto-generates
|
|
111
|
+
* BuildConfig fields — so if that file is absent in CI, fields won't be generated.
|
|
112
|
+
*/
|
|
113
|
+
function detectSecretsPlugin(content) {
|
|
114
|
+
return (/alias\s*\(\s*libs\.plugins\.secrets\s*\)/.test(content) ||
|
|
115
|
+
/id\s*\(\s*["']com\.google\.android\.libraries\.mapsplatform\.secrets-gradle-plugin["']\s*\)/.test(content) ||
|
|
116
|
+
/id\s*["']com\.google\.android\.libraries\.mapsplatform\.secrets-gradle-plugin["']/.test(content));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extracts the `propertiesFileName` from a `secrets { }` block.
|
|
120
|
+
* Defaults to "local.properties" if not configured.
|
|
121
|
+
*/
|
|
122
|
+
function extractSecretsPropertiesFile(content) {
|
|
123
|
+
const block = extractBlockContent(content, "secrets");
|
|
124
|
+
if (!block)
|
|
125
|
+
return "local.properties";
|
|
126
|
+
const m = /propertiesFileName\s*=\s*["']([^"']+)["']/.exec(block);
|
|
127
|
+
return m ? m[1] : "local.properties";
|
|
128
|
+
}
|
|
129
|
+
/** Parse property keys from a standard .properties file, skipping comments and blank lines. */
|
|
130
|
+
function parsePropertyKeys(propertiesContent) {
|
|
131
|
+
return propertiesContent
|
|
132
|
+
.split("\n")
|
|
133
|
+
.map((l) => l.trim())
|
|
134
|
+
.filter((l) => l && !l.startsWith("#") && l.includes("="))
|
|
135
|
+
.map((l) => l.split("=")[0].trim())
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
/** Returns true if the GMS google-services plugin is applied in this content. */
|
|
139
|
+
function detectGmsPlugin(content) {
|
|
140
|
+
return (/apply\s+plugin\s*:\s*['"]com\.google\.gms\.google-services['"]/.test(content) ||
|
|
141
|
+
/id\s*\(\s*["']com\.google\.gms\.google-services["']\s*\)/.test(content) ||
|
|
142
|
+
/id\s*["']com\.google\.gms\.google-services["']/.test(content));
|
|
143
|
+
}
|
|
144
|
+
/** Returns true if a signingConfigs block exists. */
|
|
145
|
+
function detectSigningConfigs(content) {
|
|
146
|
+
return /signingConfigs\s*\{/.test(content);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract env var and property references from within signingConfigs blocks.
|
|
150
|
+
* Looks for storeFile, storePassword, keyAlias, keyPassword assignments.
|
|
151
|
+
*/
|
|
152
|
+
function parseSigningCredentialRefs(content) {
|
|
153
|
+
const results = [];
|
|
154
|
+
const blockRe = /signingConfigs\s*\{([\s\S]*?)^\s*\}/m;
|
|
155
|
+
const match = blockRe.exec(content);
|
|
156
|
+
if (!match)
|
|
157
|
+
return results;
|
|
158
|
+
const block = match[1];
|
|
159
|
+
const blockStart = match.index;
|
|
160
|
+
// System.getenv inside signing block
|
|
161
|
+
const envRe = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
162
|
+
let m;
|
|
163
|
+
while ((m = envRe.exec(block)) !== null) {
|
|
164
|
+
results.push({ name: m[1], line: lineNumber(content, blockStart + m.index) });
|
|
165
|
+
}
|
|
166
|
+
// findProperty / properties[] inside signing block
|
|
167
|
+
const propRe = /(?:project\.)?findProperty\(\s*["']([^"']+)["']\s*\)|(?:project\.)?properties\["([^"]+)"\]/g;
|
|
168
|
+
while ((m = propRe.exec(block)) !== null) {
|
|
169
|
+
results.push({ name: m[1] ?? m[2], line: lineNumber(content, blockStart + m.index) });
|
|
170
|
+
}
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extract referenced keystore file paths from signingConfigs.
|
|
175
|
+
* Handles: storeFile file("path") and storeFile = file("path")
|
|
176
|
+
*/
|
|
177
|
+
function extractKeystorePaths(content) {
|
|
178
|
+
const paths = [];
|
|
179
|
+
const re = /storeFile\s*(?:=\s*)?file\(\s*["']([^"']+)["']\s*\)/g;
|
|
180
|
+
let m;
|
|
181
|
+
while ((m = re.exec(content)) !== null) {
|
|
182
|
+
paths.push(m[1]);
|
|
183
|
+
}
|
|
184
|
+
return paths;
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Java version detection
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
/**
|
|
190
|
+
* Parses a raw version token into a major Java version number.
|
|
191
|
+
* Handles formats: "21", "17", "1.8" (→ 8), "VERSION_21", "VERSION_1_8" (→ 8).
|
|
192
|
+
*/
|
|
193
|
+
function parseJavaVersion(raw) {
|
|
194
|
+
// JavaVersion enum: VERSION_21, VERSION_17, VERSION_1_8
|
|
195
|
+
const enumMatch = /VERSION_1_(\d)$/.exec(raw);
|
|
196
|
+
if (enumMatch)
|
|
197
|
+
return parseInt(enumMatch[1], 10);
|
|
198
|
+
const enumMatch2 = /VERSION_(\d+)$/.exec(raw);
|
|
199
|
+
if (enumMatch2)
|
|
200
|
+
return parseInt(enumMatch2[1], 10);
|
|
201
|
+
// String literal: "21", "17", "11", "1.8"
|
|
202
|
+
const legacyMatch = /^1\.(\d)$/.exec(raw.trim());
|
|
203
|
+
if (legacyMatch)
|
|
204
|
+
return parseInt(legacyMatch[1], 10);
|
|
205
|
+
const plain = parseInt(raw.trim(), 10);
|
|
206
|
+
if (!isNaN(plain))
|
|
207
|
+
return plain;
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Scans Gradle file content for Java / JVM version requirements and returns
|
|
212
|
+
* the highest version number found.
|
|
213
|
+
*
|
|
214
|
+
* Detects:
|
|
215
|
+
* - sourceCompatibility / targetCompatibility (Groovy & Kotlin DSL)
|
|
216
|
+
* - kotlinOptions { jvmTarget = "21" }
|
|
217
|
+
* - jvmToolchain(21) / jvmToolchain { languageVersion.set(JavaLanguageVersion.of(21)) }
|
|
218
|
+
* - java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
|
|
219
|
+
*/
|
|
220
|
+
function detectJavaVersionInContent(content) {
|
|
221
|
+
const candidates = [];
|
|
222
|
+
// sourceCompatibility / targetCompatibility = JavaVersion.VERSION_21 or = "21" or = JavaVersion.VERSION_1_8
|
|
223
|
+
const compatRe = /(?:sourceCompatibility|targetCompatibility)\s*[=\s]+([A-Za-z0-9._]+)/g;
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = compatRe.exec(content)) !== null) {
|
|
226
|
+
const v = parseJavaVersion(m[1]);
|
|
227
|
+
if (v)
|
|
228
|
+
candidates.push(v);
|
|
229
|
+
}
|
|
230
|
+
// jvmTarget = "21" (inside kotlinOptions or compileOptions)
|
|
231
|
+
const jvmTargetRe = /jvmTarget\s*=\s*["']([^"']+)["']/g;
|
|
232
|
+
while ((m = jvmTargetRe.exec(content)) !== null) {
|
|
233
|
+
const v = parseJavaVersion(m[1]);
|
|
234
|
+
if (v)
|
|
235
|
+
candidates.push(v);
|
|
236
|
+
}
|
|
237
|
+
// jvmToolchain(21)
|
|
238
|
+
const toolchainRe = /jvmToolchain\s*\(\s*(\d+)\s*\)/g;
|
|
239
|
+
while ((m = toolchainRe.exec(content)) !== null) {
|
|
240
|
+
candidates.push(parseInt(m[1], 10));
|
|
241
|
+
}
|
|
242
|
+
// JavaLanguageVersion.of(21)
|
|
243
|
+
const langVerRe = /JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/g;
|
|
244
|
+
while ((m = langVerRe.exec(content)) !== null) {
|
|
245
|
+
candidates.push(parseInt(m[1], 10));
|
|
246
|
+
}
|
|
247
|
+
return candidates.length > 0 ? Math.max(...candidates) : undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Returns the highest Java version requirement found across all Gradle files,
|
|
251
|
+
* or undefined if none is detected.
|
|
252
|
+
*/
|
|
253
|
+
function detectRequiredJavaVersion(gradleFiles) {
|
|
254
|
+
const versions = [];
|
|
255
|
+
for (const filePath of gradleFiles) {
|
|
256
|
+
const content = safeRead(filePath);
|
|
257
|
+
const v = detectJavaVersionInContent(content);
|
|
258
|
+
if (v !== undefined)
|
|
259
|
+
versions.push(v);
|
|
260
|
+
}
|
|
261
|
+
return versions.length > 0 ? Math.max(...versions) : undefined;
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Gitignore helper
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
const CI_RELEVANT_GITIGNORE_PATTERNS = [
|
|
267
|
+
{ pattern: /google-services\.json/, label: "google-services.json" },
|
|
268
|
+
{ pattern: /local\.properties/, label: "local.properties" },
|
|
269
|
+
{ pattern: /keystore\.properties/, label: "keystore.properties" },
|
|
270
|
+
{ pattern: /\.jks$|\.keystore$|\*\.jks|\*\.keystore/, label: "keystore files (*.jks / *.keystore)" },
|
|
271
|
+
{ pattern: /\.env$|\*\.env/, label: ".env files" },
|
|
272
|
+
];
|
|
273
|
+
function checkGitignore(root) {
|
|
274
|
+
const gitignorePath = resolve(root, ".gitignore");
|
|
275
|
+
if (!existsSync(gitignorePath))
|
|
276
|
+
return [];
|
|
277
|
+
const content = safeRead(gitignorePath);
|
|
278
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
279
|
+
const found = [];
|
|
280
|
+
for (const { pattern, label } of CI_RELEVANT_GITIGNORE_PATTERNS) {
|
|
281
|
+
if (lines.some((l) => pattern.test(l))) {
|
|
282
|
+
found.push(label);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Build variant detection
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Extracts the inner content of a named block using brace-counting.
|
|
292
|
+
* e.g. extractBlockContent(content, "buildTypes") returns everything inside buildTypes { ... }.
|
|
293
|
+
* Returns undefined if the block is not found.
|
|
294
|
+
*/
|
|
295
|
+
function extractBlockContent(content, keyword) {
|
|
296
|
+
const keywordRe = new RegExp(`\\b${keyword}\\s*\\{`);
|
|
297
|
+
const match = keywordRe.exec(content);
|
|
298
|
+
if (!match)
|
|
299
|
+
return undefined;
|
|
300
|
+
const start = content.indexOf("{", match.index);
|
|
301
|
+
let depth = 0;
|
|
302
|
+
for (let i = start; i < content.length; i++) {
|
|
303
|
+
if (content[i] === "{")
|
|
304
|
+
depth++;
|
|
305
|
+
else if (content[i] === "}") {
|
|
306
|
+
depth--;
|
|
307
|
+
if (depth === 0)
|
|
308
|
+
return content.slice(start + 1, i);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const CONTROL_KEYWORDS = new Set([
|
|
314
|
+
"if", "else", "for", "while", "when", "try", "catch", "finally",
|
|
315
|
+
"return", "val", "var", "fun", "object", "class", "interface",
|
|
316
|
+
]);
|
|
317
|
+
/**
|
|
318
|
+
* Extracts the names of direct child blocks from block content.
|
|
319
|
+
* Handles both Groovy DSL (name { }) and Kotlin DSL (create("name") { } / getByName("name") { }).
|
|
320
|
+
*/
|
|
321
|
+
function extractDirectChildNames(block) {
|
|
322
|
+
const names = [];
|
|
323
|
+
let depth = 0;
|
|
324
|
+
for (const line of block.split("\n")) {
|
|
325
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
326
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
327
|
+
if (depth === 0 && opens > 0) {
|
|
328
|
+
// Kotlin DSL: create("name") { or getByName("name") {
|
|
329
|
+
const kotlinMatch = /(?:create|getByName)\(\s*["'](\w+)["']\s*\)/.exec(line);
|
|
330
|
+
if (kotlinMatch) {
|
|
331
|
+
names.push(kotlinMatch[1]);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Groovy DSL: name { (identifier followed by optional whitespace then {)
|
|
335
|
+
const groovyMatch = /^\s*([a-z]\w*)\s*(?:\.\w+\s*)?\{/.exec(line);
|
|
336
|
+
if (groovyMatch && !CONTROL_KEYWORDS.has(groovyMatch[1])) {
|
|
337
|
+
names.push(groovyMatch[1]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
depth += opens - closes;
|
|
342
|
+
if (depth < 0)
|
|
343
|
+
depth = 0;
|
|
344
|
+
}
|
|
345
|
+
return [...new Set(names)];
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Detects build types and product flavors from Gradle files by static parsing.
|
|
349
|
+
* Always includes "debug" and "release" as defaults.
|
|
350
|
+
*/
|
|
351
|
+
export function detectBuildVariants(gradleFiles) {
|
|
352
|
+
const buildTypeSet = new Set(["debug", "release"]);
|
|
353
|
+
const productFlavorSet = new Set();
|
|
354
|
+
for (const filePath of gradleFiles) {
|
|
355
|
+
const content = safeRead(filePath);
|
|
356
|
+
const buildTypesBlock = extractBlockContent(content, "buildTypes");
|
|
357
|
+
if (buildTypesBlock) {
|
|
358
|
+
for (const name of extractDirectChildNames(buildTypesBlock)) {
|
|
359
|
+
buildTypeSet.add(name);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const productFlavorsBlock = extractBlockContent(content, "productFlavors");
|
|
363
|
+
if (productFlavorsBlock) {
|
|
364
|
+
for (const name of extractDirectChildNames(productFlavorsBlock)) {
|
|
365
|
+
productFlavorSet.add(name);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const buildTypes = Array.from(buildTypeSet);
|
|
370
|
+
const productFlavors = Array.from(productFlavorSet);
|
|
371
|
+
const variants = productFlavors.length === 0
|
|
372
|
+
? buildTypes
|
|
373
|
+
: productFlavors.flatMap((flavor) => buildTypes.map((bt) => `${flavor}${bt.charAt(0).toUpperCase()}${bt.slice(1)}`));
|
|
374
|
+
return { buildTypes, productFlavors, variants };
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Main scanner
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
export async function scanAndroidProject(projectRoot) {
|
|
380
|
+
const warnings = [];
|
|
381
|
+
// Track env vars we've already warned about to avoid duplicates
|
|
382
|
+
const seenEnvVars = new Set();
|
|
383
|
+
const seenProperties = new Set();
|
|
384
|
+
// Compute gitignore items upfront — used in multiple sections below
|
|
385
|
+
const gitignoreItems = checkGitignore(projectRoot);
|
|
386
|
+
// ------------------------------------------------------------------
|
|
387
|
+
// 1. Missing files
|
|
388
|
+
// ------------------------------------------------------------------
|
|
389
|
+
if (!existsSync(resolve(projectRoot, "local.properties"))) {
|
|
390
|
+
warnings.push({
|
|
391
|
+
category: "missing-file",
|
|
392
|
+
severity: "warning",
|
|
393
|
+
message: "local.properties not found",
|
|
394
|
+
hint: "CI needs ANDROID_HOME set as an env var, or sdk.dir in local.properties",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// ------------------------------------------------------------------
|
|
398
|
+
// 2. Detect required Java version and build variants
|
|
399
|
+
// ------------------------------------------------------------------
|
|
400
|
+
const gradleFiles = findGradleFiles(projectRoot);
|
|
401
|
+
const detectedJavaVersion = detectRequiredJavaVersion(gradleFiles);
|
|
402
|
+
const buildVariants = detectBuildVariants(gradleFiles);
|
|
403
|
+
// ------------------------------------------------------------------
|
|
404
|
+
// 3. Scan Gradle files
|
|
405
|
+
// ------------------------------------------------------------------
|
|
406
|
+
let gmsDetected = false;
|
|
407
|
+
let gmsDetectedIn = "";
|
|
408
|
+
let signingDetected = false;
|
|
409
|
+
let signingDetectedIn = "";
|
|
410
|
+
for (const filePath of gradleFiles) {
|
|
411
|
+
const content = safeRead(filePath);
|
|
412
|
+
const rel = relPath(projectRoot, filePath);
|
|
413
|
+
// GMS plugin
|
|
414
|
+
if (detectGmsPlugin(content)) {
|
|
415
|
+
gmsDetected = true;
|
|
416
|
+
gmsDetectedIn = rel;
|
|
417
|
+
}
|
|
418
|
+
// Signing config
|
|
419
|
+
if (detectSigningConfigs(content)) {
|
|
420
|
+
signingDetected = true;
|
|
421
|
+
signingDetectedIn = rel;
|
|
422
|
+
// Keystore file paths referenced
|
|
423
|
+
for (const kPath of extractKeystorePaths(content)) {
|
|
424
|
+
const resolved = resolve(projectRoot, kPath);
|
|
425
|
+
if (!existsSync(resolved)) {
|
|
426
|
+
warnings.push({
|
|
427
|
+
category: "missing-file",
|
|
428
|
+
severity: "warning",
|
|
429
|
+
message: `Keystore file not found: ${kPath}`,
|
|
430
|
+
hint: "Provide the keystore file as a secret file in CI or update the path in signingConfigs",
|
|
431
|
+
location: rel,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Signing credential env var / property refs
|
|
436
|
+
for (const ref of parseSigningCredentialRefs(content)) {
|
|
437
|
+
if (!seenEnvVars.has(ref.name)) {
|
|
438
|
+
seenEnvVars.add(ref.name);
|
|
439
|
+
warnings.push({
|
|
440
|
+
category: "signing-config",
|
|
441
|
+
severity: "warning",
|
|
442
|
+
message: `Signing credential referenced: ${ref.name}`,
|
|
443
|
+
hint: `Add via: ci secrets add ${ref.name}`,
|
|
444
|
+
location: `${rel}:${ref.line}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// System.getenv() references (outside buildConfigField — caught separately)
|
|
450
|
+
for (const ref of extractEnvVarRefs(content)) {
|
|
451
|
+
if (!seenEnvVars.has(ref.name)) {
|
|
452
|
+
seenEnvVars.add(ref.name);
|
|
453
|
+
warnings.push({
|
|
454
|
+
category: "env-var",
|
|
455
|
+
severity: "warning",
|
|
456
|
+
message: `Environment variable referenced: ${ref.name}`,
|
|
457
|
+
hint: `Add via: ci secrets add ${ref.name}`,
|
|
458
|
+
location: `${rel}:${ref.line}`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// buildConfigField + System.getenv references
|
|
463
|
+
for (const ref of extractBuildConfigEnvRefs(content)) {
|
|
464
|
+
if (!seenEnvVars.has(ref.name)) {
|
|
465
|
+
seenEnvVars.add(ref.name);
|
|
466
|
+
warnings.push({
|
|
467
|
+
category: "build-config",
|
|
468
|
+
severity: "warning",
|
|
469
|
+
message: `BuildConfig env var referenced: ${ref.name}`,
|
|
470
|
+
hint: `Add via: ci secrets add ${ref.name}`,
|
|
471
|
+
location: `${rel}:${ref.line}`,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Gradle property references
|
|
476
|
+
for (const ref of extractPropertyRefs(content)) {
|
|
477
|
+
if (!seenProperties.has(ref.name)) {
|
|
478
|
+
seenProperties.add(ref.name);
|
|
479
|
+
warnings.push({
|
|
480
|
+
category: "gradle-property",
|
|
481
|
+
severity: "warning",
|
|
482
|
+
message: `Gradle property referenced: ${ref.name}`,
|
|
483
|
+
hint: "Define in gradle.properties or pass as -P flag on CI",
|
|
484
|
+
location: `${rel}:${ref.line}`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// ------------------------------------------------------------------
|
|
490
|
+
// 4. Google Secrets Gradle Plugin — detect missing local.properties
|
|
491
|
+
// ------------------------------------------------------------------
|
|
492
|
+
//
|
|
493
|
+
// The Secrets plugin reads a properties file (default: local.properties) and
|
|
494
|
+
// auto-generates BuildConfig fields. If that file is gitignored / absent in CI
|
|
495
|
+
// the fields are not generated, causing "Unresolved reference" compile errors.
|
|
496
|
+
// We check once per unique properties file path across all modules.
|
|
497
|
+
// ------------------------------------------------------------------
|
|
498
|
+
const seenSecretsPropertiesFiles = new Set();
|
|
499
|
+
// First pass: collect the defaultPropertiesFileName from any build file that sets it.
|
|
500
|
+
// This is typically "secrets.defaults.properties" in the app module.
|
|
501
|
+
let secretsDefaultsFileName;
|
|
502
|
+
for (const filePath of gradleFiles) {
|
|
503
|
+
const content = safeRead(filePath);
|
|
504
|
+
const block = extractBlockContent(content, "secrets");
|
|
505
|
+
if (!block)
|
|
506
|
+
continue;
|
|
507
|
+
const m = /defaultPropertiesFileName\s*=\s*["']([^"']+)["']/.exec(block);
|
|
508
|
+
if (m) {
|
|
509
|
+
secretsDefaultsFileName = m[1];
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Also look for any *.defaults.properties file in the project root as a fallback
|
|
514
|
+
if (!secretsDefaultsFileName) {
|
|
515
|
+
const candidates = ["secrets.defaults.properties", "local.defaults.properties"];
|
|
516
|
+
for (const c of candidates) {
|
|
517
|
+
if (existsSync(resolve(projectRoot, c))) {
|
|
518
|
+
secretsDefaultsFileName = c;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const filePath of gradleFiles) {
|
|
524
|
+
const content = safeRead(filePath);
|
|
525
|
+
if (!detectSecretsPlugin(content))
|
|
526
|
+
continue;
|
|
527
|
+
const propsFileName = extractSecretsPropertiesFile(content);
|
|
528
|
+
if (seenSecretsPropertiesFiles.has(propsFileName))
|
|
529
|
+
continue;
|
|
530
|
+
seenSecretsPropertiesFiles.add(propsFileName);
|
|
531
|
+
// Warn when the properties file is gitignored — it won't be present on a clean CI machine
|
|
532
|
+
// even if it exists locally (e.g. local.properties with only sdk.dir).
|
|
533
|
+
const isGitignored = gitignoreItems.some((item) => item.includes(propsFileName));
|
|
534
|
+
if (!isGitignored)
|
|
535
|
+
continue;
|
|
536
|
+
// Find expected keys from the companion defaults file
|
|
537
|
+
const expectedKeys = secretsDefaultsFileName
|
|
538
|
+
? parsePropertyKeys(safeRead(resolve(projectRoot, secretsDefaultsFileName)))
|
|
539
|
+
: [];
|
|
540
|
+
if (expectedKeys.length > 0) {
|
|
541
|
+
// Emit one warning per expected key so it shows up in the missing-secrets hint
|
|
542
|
+
for (const key of expectedKeys) {
|
|
543
|
+
warnings.push({
|
|
544
|
+
category: "secrets-plugin",
|
|
545
|
+
severity: "warning",
|
|
546
|
+
message: `Secrets plugin key: ${key}`,
|
|
547
|
+
hint: `Add via: ci secrets add ${key}`,
|
|
548
|
+
location: propsFileName,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
warnings.push({
|
|
554
|
+
category: "secrets-plugin",
|
|
555
|
+
severity: "warning",
|
|
556
|
+
message: `Google Secrets Gradle Plugin reads '${propsFileName}' which is absent in CI`,
|
|
557
|
+
hint: `Inject '${propsFileName}' as a file step using a secret variable`,
|
|
558
|
+
location: relPath(projectRoot, filePath),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// ------------------------------------------------------------------
|
|
563
|
+
// 5. Firebase / GMS — check google-services.json after scanning all files
|
|
564
|
+
// ------------------------------------------------------------------
|
|
565
|
+
if (gmsDetected) {
|
|
566
|
+
// google-services.json can live in the root or in the app module dir
|
|
567
|
+
const candidatePaths = [
|
|
568
|
+
resolve(projectRoot, "google-services.json"),
|
|
569
|
+
resolve(projectRoot, "app", "google-services.json"),
|
|
570
|
+
];
|
|
571
|
+
const found = candidatePaths.some((p) => existsSync(p));
|
|
572
|
+
if (!found) {
|
|
573
|
+
warnings.push({
|
|
574
|
+
category: "firebase",
|
|
575
|
+
severity: "warning",
|
|
576
|
+
message: `google-services.json not found (GMS plugin detected in ${gmsDetectedIn})`,
|
|
577
|
+
hint: "Add google-services.json as a secret file or generate it from env vars at build time",
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// ------------------------------------------------------------------
|
|
582
|
+
// 5. Signing config summary warning (if detected and not already specific)
|
|
583
|
+
// ------------------------------------------------------------------
|
|
584
|
+
if (signingDetected) {
|
|
585
|
+
warnings.push({
|
|
586
|
+
category: "signing-config",
|
|
587
|
+
severity: "info",
|
|
588
|
+
message: `signingConfigs block found in ${signingDetectedIn}`,
|
|
589
|
+
hint: "Ensure keystore file and all signing credentials are available in the CI environment",
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
// ------------------------------------------------------------------
|
|
593
|
+
// 6. Gitignore cross-check
|
|
594
|
+
// ------------------------------------------------------------------
|
|
595
|
+
for (const item of gitignoreItems) {
|
|
596
|
+
// Only warn if we haven't already emitted a more specific warning for it
|
|
597
|
+
const alreadyCovered = (item === "google-services.json" && gmsDetected) ||
|
|
598
|
+
(item === "local.properties" && !existsSync(resolve(projectRoot, "local.properties")));
|
|
599
|
+
if (!alreadyCovered) {
|
|
600
|
+
warnings.push({
|
|
601
|
+
category: "missing-file",
|
|
602
|
+
severity: "info",
|
|
603
|
+
message: `${item} is in .gitignore — likely absent in CI`,
|
|
604
|
+
hint: "Provide this file as a secret or generate it at build time",
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return { warnings, detectedJavaVersion, buildVariants };
|
|
609
|
+
}
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// Formatter
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
const CATEGORY_LABELS = {
|
|
614
|
+
"missing-file": "Missing files",
|
|
615
|
+
"env-var": "Environment variables referenced in Gradle files",
|
|
616
|
+
"gradle-property": "Gradle properties referenced",
|
|
617
|
+
"signing-config": "Signing configuration",
|
|
618
|
+
"build-config": "BuildConfig env vars",
|
|
619
|
+
"firebase": "Firebase / GMS",
|
|
620
|
+
"secrets-plugin": "Google Secrets Gradle Plugin (local.properties keys)",
|
|
621
|
+
};
|
|
622
|
+
export function formatScanResult(result) {
|
|
623
|
+
const lines = [];
|
|
624
|
+
const sep = "─".repeat(50);
|
|
625
|
+
// Always show the detected Java version
|
|
626
|
+
if (result.detectedJavaVersion !== undefined) {
|
|
627
|
+
lines.push(`ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${result.detectedJavaVersion}`);
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
if (result.warnings.length === 0) {
|
|
631
|
+
lines.push("✅ No potential unknowns detected.");
|
|
632
|
+
return lines.join("\n") + "\n";
|
|
633
|
+
}
|
|
634
|
+
lines.push(`⚠ Android Project Scan — Potential Unknowns`);
|
|
635
|
+
lines.push(sep);
|
|
636
|
+
lines.push("");
|
|
637
|
+
// Group by category
|
|
638
|
+
const byCategory = new Map();
|
|
639
|
+
for (const w of result.warnings) {
|
|
640
|
+
if (!byCategory.has(w.category))
|
|
641
|
+
byCategory.set(w.category, []);
|
|
642
|
+
byCategory.get(w.category).push(w);
|
|
643
|
+
}
|
|
644
|
+
for (const [cat, items] of byCategory) {
|
|
645
|
+
lines.push(`${CATEGORY_LABELS[cat]}:`);
|
|
646
|
+
for (const w of items) {
|
|
647
|
+
const icon = w.severity === "warning" ? "⚠ " : "ℹ ";
|
|
648
|
+
const loc = w.location ? ` (${w.location})` : "";
|
|
649
|
+
lines.push(` ${icon} ${w.message}${loc}`);
|
|
650
|
+
if (w.hint) {
|
|
651
|
+
lines.push(` Hint: ${w.hint}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
lines.push("");
|
|
655
|
+
}
|
|
656
|
+
lines.push(sep);
|
|
657
|
+
const warnCount = result.warnings.filter((w) => w.severity === "warning").length;
|
|
658
|
+
const infoCount = result.warnings.filter((w) => w.severity === "info").length;
|
|
659
|
+
const parts = [];
|
|
660
|
+
if (warnCount > 0)
|
|
661
|
+
parts.push(`${warnCount} warning(s)`);
|
|
662
|
+
if (infoCount > 0)
|
|
663
|
+
parts.push(`${infoCount} info`);
|
|
664
|
+
lines.push(`${parts.join(", ")} found. Review before running on CI.`);
|
|
665
|
+
return lines.join("\n");
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=android-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAw9BA,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,IAAI,EAClE,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACvE,OAAO,CAAC,IAAI,CAAC,CAqRf"}
|