@invarn/cibuild 2.6.5 → 2.6.7

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A plugin applied inside `if (file("…").exists()) { … }` is conditional on a
3
+ * file the build may not have. Counting it as a hard credential requirement
4
+ * sends the user looking for a file the build never asks for — and, when the
5
+ * same rule is used to predict outcomes, reports projects as blocked that
6
+ * build green with no credential at all.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=android-gms-guard.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-gms-guard.test.d.ts","sourceRoot":"","sources":["../../../src/commands/android-gms-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A plugin applied inside `if (file("…").exists()) { … }` is conditional on a
3
+ * file the build may not have. Counting it as a hard credential requirement
4
+ * sends the user looking for a file the build never asks for — and, when the
5
+ * same rule is used to predict outcomes, reports projects as blocked that
6
+ * build green with no credential at all.
7
+ */
8
+ import { describe, test, expect } from '@jest/globals';
9
+ import { stripExistenceGuardedBlocks } from './android-scanner.js';
10
+ describe('stripExistenceGuardedBlocks', () => {
11
+ test('removes a Kotlin DSL existence guard and its body', () => {
12
+ const content = `
13
+ plugins {
14
+ id("com.android.application")
15
+ }
16
+
17
+ if (file("google-services.json").exists()) {
18
+ apply(plugin = libs.plugins.gms.googleServices.get().pluginId)
19
+ }
20
+ `;
21
+ const stripped = stripExistenceGuardedBlocks(content);
22
+ expect(stripped).toContain('com.android.application');
23
+ expect(stripped).not.toContain('googleServices');
24
+ });
25
+ test('removes a Groovy existence guard', () => {
26
+ const content = `
27
+ if (file('google-services.json').exists()) {
28
+ apply plugin: 'com.google.gms.google-services'
29
+ }
30
+ `;
31
+ expect(stripExistenceGuardedBlocks(content)).not.toContain('google-services');
32
+ });
33
+ test('removes a guard qualified by a project receiver', () => {
34
+ const content = `
35
+ if (rootProject.file("app/google-services.json").exists()) {
36
+ apply(plugin = "com.google.gms.google-services")
37
+ }
38
+ `;
39
+ expect(stripExistenceGuardedBlocks(content)).not.toContain('google-services');
40
+ });
41
+ test('keeps nested braces inside the guard body together', () => {
42
+ const content = `
43
+ if (file("google-services.json").exists()) {
44
+ android { buildTypes { release { } } }
45
+ apply(plugin = "com.google.gms.google-services")
46
+ }
47
+ val keep = "after"
48
+ `;
49
+ const stripped = stripExistenceGuardedBlocks(content);
50
+ expect(stripped).not.toContain('google-services');
51
+ expect(stripped).toContain('val keep = "after"');
52
+ });
53
+ test('is not fooled by a brace inside a string literal', () => {
54
+ const content = `
55
+ if (file("google-services.json").exists()) {
56
+ val template = "\${'{'}not a block"
57
+ apply(plugin = "com.google.gms.google-services")
58
+ }
59
+ val keep = "after"
60
+ `;
61
+ const stripped = stripExistenceGuardedBlocks(content);
62
+ expect(stripped).not.toContain('google-services');
63
+ expect(stripped).toContain('val keep = "after"');
64
+ });
65
+ test('leaves an unguarded declaration exactly where it was', () => {
66
+ const content = `
67
+ plugins {
68
+ id("com.google.gms.google-services")
69
+ }
70
+ `;
71
+ expect(stripExistenceGuardedBlocks(content)).toBe(content);
72
+ });
73
+ test('leaves content alone when the guard never closes', () => {
74
+ const content = 'if (file("google-services.json").exists()) {\n apply(plugin = "x")\n';
75
+ expect(stripExistenceGuardedBlocks(content)).toBe(content);
76
+ });
77
+ });
78
+ //# sourceMappingURL=android-gms-guard.test.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Which JDK a generated Android pipeline asks for.
3
+ *
4
+ * Two constraints decide it and the old `Math.max(detected ?? 17, 17)` knew
5
+ * neither: build machines install a fixed set of JDKs, and Gradle refuses to
6
+ * start on a JDK newer than it knows about. Naming a JDK that is not
7
+ * installed fails at "Set the Java version", before a line compiles; raising
8
+ * a project onto a JDK its Gradle cannot run makes an old wrapper
9
+ * unbuildable by construction.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=android-java-version.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-java-version.test.d.ts","sourceRoot":"","sources":["../../../src/commands/android-java-version.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Which JDK a generated Android pipeline asks for.
3
+ *
4
+ * Two constraints decide it and the old `Math.max(detected ?? 17, 17)` knew
5
+ * neither: build machines install a fixed set of JDKs, and Gradle refuses to
6
+ * start on a JDK newer than it knows about. Naming a JDK that is not
7
+ * installed fails at "Set the Java version", before a line compiles; raising
8
+ * a project onto a JDK its Gradle cannot run makes an old wrapper
9
+ * unbuildable by construction.
10
+ */
11
+ import { describe, test, expect } from '@jest/globals';
12
+ import { AVAILABLE_JAVA_VERSIONS, gradleJavaCeiling, gradleJavaFloor, javaVersionProblem, parseGradleWrapperVersion, runnableJavaVersion, } from './android-scanner.js';
13
+ describe('parseGradleWrapperVersion', () => {
14
+ test('reads the version out of a distributionUrl', () => {
15
+ expect(parseGradleWrapperVersion('distributionUrl=https\\://services.gradle.org/distributions/gradle-8.9-bin.zip')).toBe('8.9');
16
+ });
17
+ test('reads an -all distribution and a three-part version', () => {
18
+ expect(parseGradleWrapperVersion('distributionUrl=https\\://services.gradle.org/distributions/gradle-5.2.1-all.zip')).toBe('5.2.1');
19
+ });
20
+ test('reads a release-candidate version', () => {
21
+ expect(parseGradleWrapperVersion('distributionUrl=https\\://services.gradle.org/distributions/gradle-9.0-rc-1-bin.zip')).toBe('9.0-rc-1');
22
+ });
23
+ test('returns undefined for a properties file that pins nothing', () => {
24
+ expect(parseGradleWrapperVersion('distributionBase=GRADLE_USER_HOME')).toBeUndefined();
25
+ expect(parseGradleWrapperVersion('')).toBeUndefined();
26
+ });
27
+ });
28
+ describe('gradleJavaCeiling / gradleJavaFloor', () => {
29
+ test('tracks what each Gradle line can run on', () => {
30
+ expect(gradleJavaCeiling('4.10.2')).toBe(8);
31
+ expect(gradleJavaCeiling('5.2.1')).toBe(11);
32
+ expect(gradleJavaCeiling('6.9')).toBe(15);
33
+ expect(gradleJavaCeiling('7.6.1')).toBe(17);
34
+ expect(gradleJavaCeiling('8.9')).toBe(21);
35
+ expect(gradleJavaCeiling('9.4.0')).toBe(24);
36
+ });
37
+ test('knows Gradle 9 dropped everything below 17', () => {
38
+ expect(gradleJavaFloor('8.9')).toBe(8);
39
+ expect(gradleJavaFloor('9.0')).toBe(17);
40
+ });
41
+ test('says nothing about a version it cannot read', () => {
42
+ expect(gradleJavaCeiling(undefined)).toBeUndefined();
43
+ expect(gradleJavaCeiling('nightly')).toBeUndefined();
44
+ expect(gradleJavaFloor(undefined)).toBeUndefined();
45
+ });
46
+ });
47
+ describe('runnableJavaVersion', () => {
48
+ test('stays on the default when the default covers the declaration', () => {
49
+ // 11 is installed, but AGP 8.x cannot run on it — a project declaring
50
+ // sourceCompatibility 11 must still be built on 17.
51
+ expect(runnableJavaVersion(11)).toBe(17);
52
+ expect(runnableJavaVersion(17)).toBe(17);
53
+ expect(runnableJavaVersion(21)).toBe(21);
54
+ });
55
+ test('defaults to the preferred JDK when nothing is declared', () => {
56
+ expect(runnableJavaVersion(undefined)).toBe(17);
57
+ });
58
+ test('steps down to 11 only when the wrapper rules the default out', () => {
59
+ expect(runnableJavaVersion(undefined, '5.2.1')).toBe(11);
60
+ expect(runnableJavaVersion(11, '5.2.1')).toBe(11);
61
+ expect(runnableJavaVersion(undefined, '6.9')).toBe(11);
62
+ });
63
+ test('never names a JDK no build machine has', () => {
64
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(23));
65
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(25, '8.9'));
66
+ });
67
+ test('lets an old wrapper cap a newer declaration', () => {
68
+ // Gradle 7.x tops out at 17 even though 21 is installed.
69
+ expect(runnableJavaVersion(21, '7.6.1')).toBe(17);
70
+ });
71
+ test('honours the Gradle 9 floor of 17', () => {
72
+ expect(runnableJavaVersion(8, '9.0')).toBe(17);
73
+ });
74
+ });
75
+ describe('javaVersionProblem', () => {
76
+ test('names a Gradle version no installed JDK can run', () => {
77
+ // 4.10.2 tops out at Java 8, and no image carries 8.
78
+ const problem = javaVersionProblem(undefined, '4.10.2');
79
+ expect(problem).toContain('Gradle 4.10.2');
80
+ expect(problem).toContain('Java 8');
81
+ });
82
+ test('names a declaration above every installed JDK', () => {
83
+ const problem = javaVersionProblem(23, '8.9');
84
+ expect(problem).toContain('Java 23');
85
+ expect(problem).toContain('11, 17 and 21');
86
+ });
87
+ test('is silent when every constraint was honoured', () => {
88
+ expect(javaVersionProblem(17, '8.9')).toBeUndefined();
89
+ expect(javaVersionProblem(undefined, '8.9')).toBeUndefined();
90
+ expect(javaVersionProblem(undefined, undefined)).toBeUndefined();
91
+ // 5.2.1 used to be a problem. The images carry 11 now, so it is not.
92
+ expect(javaVersionProblem(undefined, '5.2.1')).toBeUndefined();
93
+ });
94
+ });
95
+ //# sourceMappingURL=android-java-version.test.js.map
@@ -11,6 +11,8 @@ export interface ScanResult {
11
11
  warnings: BuildWarning[];
12
12
  /** Highest Java version requirement detected in Gradle files (e.g. 17, 21). */
13
13
  detectedJavaVersion?: number;
14
+ /** Gradle version the wrapper pins, e.g. "8.9". Undefined when there is no wrapper. */
15
+ detectedGradleVersion?: string;
14
16
  /** Build types, product flavors, and all variant combinations detected in Gradle files. */
15
17
  buildVariants: BuildVariants;
16
18
  }
@@ -22,6 +24,88 @@ export interface BuildVariants {
22
24
  /** All variant combinations. Equals buildTypes when no product flavors are defined. */
23
25
  variants: string[];
24
26
  }
27
+ /**
28
+ * Removes `if (file("…").exists()) { … }` blocks from build-file content.
29
+ *
30
+ * A plugin applied inside one of these is *conditional on a file the build
31
+ * may not have*. Projects use the idiom precisely so a checkout without the
32
+ * credential still builds:
33
+ *
34
+ * if (file("google-services.json").exists()) {
35
+ * apply(plugin = libs.plugins.gms.googleServices.get().pluginId)
36
+ * }
37
+ *
38
+ * Reading the declaration and ignoring the guard turns "this project builds
39
+ * fine without a credential" into "this project cannot build without one" —
40
+ * a warning that sends the user looking for a file the build never asks for.
41
+ *
42
+ * Brace matching skips quoted strings so a `{` inside a literal cannot end
43
+ * the block early. Anything it cannot match to a close brace is left alone:
44
+ * losing a real declaration is worse than keeping a guarded one.
45
+ */
46
+ export declare function stripExistenceGuardedBlocks(content: string): string;
47
+ /**
48
+ * The Gradle version a wrapper pins, read out of its `distributionUrl`.
49
+ * `…/gradle-8.9-bin.zip` → `8.9`.
50
+ *
51
+ * This bounds which JDK the project can build on: Gradle refuses to start on
52
+ * a JDK newer than it knows about, so the wrapper is a hard ceiling that no
53
+ * `sourceCompatibility` declaration can raise.
54
+ *
55
+ * The trailing `-bin` / `-all` belongs to the file name rather than the
56
+ * version, and a release candidate spends two more hyphens on `-rc-1`, so the
57
+ * version group has to be able to give its last segment back.
58
+ */
59
+ export declare function parseGradleWrapperVersion(content: string): string | undefined;
60
+ /**
61
+ * The newest Java release a given Gradle version can run on, or undefined
62
+ * when the version string says nothing usable.
63
+ */
64
+ export declare function gradleJavaCeiling(gradleVersion: string | undefined): number | undefined;
65
+ /** The oldest Java release a given Gradle version will start on. */
66
+ export declare function gradleJavaFloor(gradleVersion: string | undefined): number | undefined;
67
+ /**
68
+ * The JDKs a build machine installs, and therefore the only values the
69
+ * generated `set-java-version` step can satisfy.
70
+ *
71
+ * 11 joined the images on 2026-09-05, for Gradle 5.x and 6.x — which cannot
72
+ * *start* on 17, whatever the sources declare. It is a fallback, never a
73
+ * preference; see `PREFERRED_JAVA_VERSION`.
74
+ */
75
+ export declare const AVAILABLE_JAVA_VERSIONS: number[];
76
+ /**
77
+ * The JDK to write whenever nothing forces otherwise.
78
+ *
79
+ * This is what makes 11 safe to list. The selection below used to answer with
80
+ * the *lowest* usable JDK, which was harmless while the lowest was 17. With 11
81
+ * installed that rule would write `JAVA_VERSION: 11` for every project whose
82
+ * `sourceCompatibility` is 11 — and AGP 8.x will not run on JDK 11, so
83
+ * pipelines that build today would stop building.
84
+ */
85
+ export declare const PREFERRED_JAVA_VERSION = 17;
86
+ /**
87
+ * The Java version to write into a generated pipeline: `PREFERRED_JAVA_VERSION`
88
+ * whenever it satisfies both what the sources declare and what the Gradle
89
+ * wrapper can run on, and the nearest available JDK that does when it cannot.
90
+ *
91
+ * Source compatibility is a floor, not an exact request — a project
92
+ * declaring 11 compiles fine on 17 — but it is not the only constraint.
93
+ * Gradle starts before it reads a single `sourceCompatibility`, so a wrapper
94
+ * pinning an old Gradle caps the JDK no matter what the sources say, and a
95
+ * declaration above every installed JDK cannot be honoured at all.
96
+ *
97
+ * Falls back to the preferred JDK — not the lowest — when the constraints
98
+ * cannot all be met, because a pipeline has to name something and the
99
+ * unsatisfiable cases are overwhelmingly "declared something newer than we
100
+ * have", where 17 is a far better guess than 11. `javaVersionProblem` says
101
+ * what was given up so the caller can warn.
102
+ */
103
+ export declare function runnableJavaVersion(detectedJavaVersion: number | undefined, gradleVersion?: string): number;
104
+ /**
105
+ * What `runnableJavaVersion` could not honour, as a sentence, or undefined
106
+ * when it honoured everything.
107
+ */
108
+ export declare function javaVersionProblem(detectedJavaVersion: number | undefined, gradleVersion?: string): string | undefined;
25
109
  /**
26
110
  * Detects build types and product flavors from Gradle files by static parsing.
27
111
  * Always includes "debug" and "release" as defaults.
@@ -1 +1 @@
1
- {"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,SAAS,GACT,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2FAA2F;IAC3F,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wFAAwF;IACxF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAoYD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CAmCxE;AAMD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA2PjF;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAgD3D"}
1
+ {"version":3,"file":"android-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/android-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,SAAS,GACT,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2FAA2F;IAC3F,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wFAAwF;IACxF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA+JD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnE;AAoKD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG7E;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASvF;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIrF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,UAAe,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAYR;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAkBpB;AA6FD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CAmCxE;AAMD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA4PjF;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAyD3D"}
@@ -135,11 +135,74 @@ function parsePropertyKeys(propertiesContent) {
135
135
  .map((l) => l.split("=")[0].trim())
136
136
  .filter(Boolean);
137
137
  }
138
- /** Returns true if the GMS google-services plugin is applied in this content. */
138
+ /**
139
+ * Removes `if (file("…").exists()) { … }` blocks from build-file content.
140
+ *
141
+ * A plugin applied inside one of these is *conditional on a file the build
142
+ * may not have*. Projects use the idiom precisely so a checkout without the
143
+ * credential still builds:
144
+ *
145
+ * if (file("google-services.json").exists()) {
146
+ * apply(plugin = libs.plugins.gms.googleServices.get().pluginId)
147
+ * }
148
+ *
149
+ * Reading the declaration and ignoring the guard turns "this project builds
150
+ * fine without a credential" into "this project cannot build without one" —
151
+ * a warning that sends the user looking for a file the build never asks for.
152
+ *
153
+ * Brace matching skips quoted strings so a `{` inside a literal cannot end
154
+ * the block early. Anything it cannot match to a close brace is left alone:
155
+ * losing a real declaration is worse than keeping a guarded one.
156
+ */
157
+ export function stripExistenceGuardedBlocks(content) {
158
+ const guard = /if\s*\(\s*[A-Za-z0-9_.]*file\s*\(\s*[^)]*\)\s*\.exists\s*\(\s*\)\s*\)\s*\{/g;
159
+ let out = "";
160
+ let cursor = 0;
161
+ let match;
162
+ while ((match = guard.exec(content)) !== null) {
163
+ const bodyStart = match.index + match[0].length;
164
+ const end = matchingBrace(content, bodyStart);
165
+ if (end === -1)
166
+ continue;
167
+ out += content.slice(cursor, match.index);
168
+ cursor = end + 1;
169
+ guard.lastIndex = cursor;
170
+ }
171
+ return out + content.slice(cursor);
172
+ }
173
+ /** Index of the `}` closing a block whose body starts at `start`, or -1. */
174
+ function matchingBrace(content, start) {
175
+ let depth = 1;
176
+ let quote = null;
177
+ for (let i = start; i < content.length; i++) {
178
+ const ch = content[i];
179
+ if (quote) {
180
+ if (ch === "\\")
181
+ i++;
182
+ else if (ch === quote)
183
+ quote = null;
184
+ continue;
185
+ }
186
+ if (ch === '"' || ch === "'")
187
+ quote = ch;
188
+ else if (ch === "{")
189
+ depth++;
190
+ else if (ch === "}" && --depth === 0)
191
+ return i;
192
+ }
193
+ return -1;
194
+ }
195
+ /**
196
+ * Returns true if the GMS google-services plugin is applied unconditionally
197
+ * in this content. A declaration guarded by the presence of
198
+ * `google-services.json` is not a credential requirement — see
199
+ * `stripExistenceGuardedBlocks`.
200
+ */
139
201
  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));
202
+ const unguarded = stripExistenceGuardedBlocks(content);
203
+ return (/apply\s+plugin\s*:\s*['"]com\.google\.gms\.google-services['"]/.test(unguarded) ||
204
+ /id\s*\(\s*["']com\.google\.gms\.google-services["']\s*\)/.test(unguarded) ||
205
+ /id\s*["']com\.google\.gms\.google-services["']/.test(unguarded));
143
206
  }
144
207
  /** Returns true if a signingConfigs block exists. */
145
208
  function detectSigningConfigs(content) {
@@ -260,6 +323,126 @@ function detectRequiredJavaVersion(gradleFiles) {
260
323
  }
261
324
  return versions.length > 0 ? Math.max(...versions) : undefined;
262
325
  }
326
+ /**
327
+ * The Gradle version a wrapper pins, read out of its `distributionUrl`.
328
+ * `…/gradle-8.9-bin.zip` → `8.9`.
329
+ *
330
+ * This bounds which JDK the project can build on: Gradle refuses to start on
331
+ * a JDK newer than it knows about, so the wrapper is a hard ceiling that no
332
+ * `sourceCompatibility` declaration can raise.
333
+ *
334
+ * The trailing `-bin` / `-all` belongs to the file name rather than the
335
+ * version, and a release candidate spends two more hyphens on `-rc-1`, so the
336
+ * version group has to be able to give its last segment back.
337
+ */
338
+ export function parseGradleWrapperVersion(content) {
339
+ const match = /gradle-(\d+(?:\.\d+)*(?:-[A-Za-z0-9.]+)*)-(?:bin|all)\.zip/.exec(content);
340
+ return match ? match[1] : undefined;
341
+ }
342
+ function detectGradleWrapperVersion(projectRoot) {
343
+ const path = resolve(projectRoot, "gradle", "wrapper", "gradle-wrapper.properties");
344
+ if (!existsSync(path))
345
+ return undefined;
346
+ return parseGradleWrapperVersion(safeRead(path));
347
+ }
348
+ /**
349
+ * The newest Java release a given Gradle version can run on, or undefined
350
+ * when the version string says nothing usable.
351
+ */
352
+ export function gradleJavaCeiling(gradleVersion) {
353
+ const major = Number(/^(\d+)\./.exec(gradleVersion ?? "")?.[1]);
354
+ if (!Number.isInteger(major))
355
+ return undefined;
356
+ if (major <= 4)
357
+ return 8;
358
+ if (major === 5)
359
+ return 11;
360
+ if (major === 6)
361
+ return 15;
362
+ if (major === 7)
363
+ return 17;
364
+ if (major === 8)
365
+ return 21;
366
+ return 24;
367
+ }
368
+ /** The oldest Java release a given Gradle version will start on. */
369
+ export function gradleJavaFloor(gradleVersion) {
370
+ const major = Number(/^(\d+)\./.exec(gradleVersion ?? "")?.[1]);
371
+ if (!Number.isInteger(major))
372
+ return undefined;
373
+ return major >= 9 ? 17 : 8;
374
+ }
375
+ /**
376
+ * The JDKs a build machine installs, and therefore the only values the
377
+ * generated `set-java-version` step can satisfy.
378
+ *
379
+ * 11 joined the images on 2026-09-05, for Gradle 5.x and 6.x — which cannot
380
+ * *start* on 17, whatever the sources declare. It is a fallback, never a
381
+ * preference; see `PREFERRED_JAVA_VERSION`.
382
+ */
383
+ export const AVAILABLE_JAVA_VERSIONS = [11, 17, 21];
384
+ /**
385
+ * The JDK to write whenever nothing forces otherwise.
386
+ *
387
+ * This is what makes 11 safe to list. The selection below used to answer with
388
+ * the *lowest* usable JDK, which was harmless while the lowest was 17. With 11
389
+ * installed that rule would write `JAVA_VERSION: 11` for every project whose
390
+ * `sourceCompatibility` is 11 — and AGP 8.x will not run on JDK 11, so
391
+ * pipelines that build today would stop building.
392
+ */
393
+ export const PREFERRED_JAVA_VERSION = 17;
394
+ /**
395
+ * The Java version to write into a generated pipeline: `PREFERRED_JAVA_VERSION`
396
+ * whenever it satisfies both what the sources declare and what the Gradle
397
+ * wrapper can run on, and the nearest available JDK that does when it cannot.
398
+ *
399
+ * Source compatibility is a floor, not an exact request — a project
400
+ * declaring 11 compiles fine on 17 — but it is not the only constraint.
401
+ * Gradle starts before it reads a single `sourceCompatibility`, so a wrapper
402
+ * pinning an old Gradle caps the JDK no matter what the sources say, and a
403
+ * declaration above every installed JDK cannot be honoured at all.
404
+ *
405
+ * Falls back to the preferred JDK — not the lowest — when the constraints
406
+ * cannot all be met, because a pipeline has to name something and the
407
+ * unsatisfiable cases are overwhelmingly "declared something newer than we
408
+ * have", where 17 is a far better guess than 11. `javaVersionProblem` says
409
+ * what was given up so the caller can warn.
410
+ */
411
+ export function runnableJavaVersion(detectedJavaVersion, gradleVersion) {
412
+ const ceiling = gradleJavaCeiling(gradleVersion) ?? Infinity;
413
+ const floor = Math.max(detectedJavaVersion ?? 0, gradleJavaFloor(gradleVersion) ?? 0);
414
+ const usable = AVAILABLE_JAVA_VERSIONS.filter((v) => v >= floor && v <= ceiling).sort((a, b) => a - b);
415
+ if (usable.length === 0)
416
+ return PREFERRED_JAVA_VERSION;
417
+ if (usable.includes(PREFERRED_JAVA_VERSION))
418
+ return PREFERRED_JAVA_VERSION;
419
+ // Forced off the default: below it take the newest that fits under the
420
+ // ceiling, above it the oldest that clears the floor.
421
+ const below = usable.filter((v) => v < PREFERRED_JAVA_VERSION);
422
+ return below.length > 0 ? below[below.length - 1] : usable[0];
423
+ }
424
+ /**
425
+ * What `runnableJavaVersion` could not honour, as a sentence, or undefined
426
+ * when it honoured everything.
427
+ */
428
+ export function javaVersionProblem(detectedJavaVersion, gradleVersion) {
429
+ // "11, 17 and 21", not "11 and 17 and 21" — a human reads this next to a
430
+ // build that will not start.
431
+ const sorted = [...AVAILABLE_JAVA_VERSIONS].sort((a, b) => a - b);
432
+ const installed = sorted.length > 1
433
+ ? `${sorted.slice(0, -1).join(", ")} and ${sorted[sorted.length - 1]}`
434
+ : String(sorted[0]);
435
+ const ceiling = gradleJavaCeiling(gradleVersion);
436
+ const lowest = Math.min(...AVAILABLE_JAVA_VERSIONS);
437
+ if (ceiling !== undefined && ceiling < lowest) {
438
+ return `Gradle ${gradleVersion} runs on Java ${ceiling} at most, and build machines carry only ${installed}. This pipeline will very likely fail before it compiles anything — upgrade the Gradle wrapper.`;
439
+ }
440
+ const highest = Math.max(...AVAILABLE_JAVA_VERSIONS);
441
+ if ((detectedJavaVersion ?? 0) > highest) {
442
+ return `This project declares Java ${detectedJavaVersion}, and build machines carry only ${installed}. Writing Java ${runnableJavaVersion(detectedJavaVersion, gradleVersion)} instead — asking for ${detectedJavaVersion} would fail at "Set the Java version".`;
443
+ }
444
+ return undefined;
445
+ }
263
446
  // ---------------------------------------------------------------------------
264
447
  // Gitignore helper
265
448
  // ---------------------------------------------------------------------------
@@ -399,6 +582,7 @@ export async function scanAndroidProject(projectRoot) {
399
582
  // ------------------------------------------------------------------
400
583
  const gradleFiles = findGradleFiles(projectRoot);
401
584
  const detectedJavaVersion = detectRequiredJavaVersion(gradleFiles);
585
+ const detectedGradleVersion = detectGradleWrapperVersion(projectRoot);
402
586
  const buildVariants = detectBuildVariants(gradleFiles);
403
587
  // ------------------------------------------------------------------
404
588
  // 3. Scan Gradle files
@@ -605,7 +789,7 @@ export async function scanAndroidProject(projectRoot) {
605
789
  });
606
790
  }
607
791
  }
608
- return { warnings, detectedJavaVersion, buildVariants };
792
+ return { warnings, detectedJavaVersion, detectedGradleVersion, buildVariants };
609
793
  }
610
794
  // ---------------------------------------------------------------------------
611
795
  // Formatter
@@ -622,9 +806,13 @@ const CATEGORY_LABELS = {
622
806
  export function formatScanResult(result) {
623
807
  const lines = [];
624
808
  const sep = "─".repeat(50);
625
- // Always show the detected Java version
809
+ // Always show the detected Java version, and the one the pipeline will
810
+ // actually name — they differ whenever the declaration is below the oldest
811
+ // installed JDK or above the newest, and saying only the first made the
812
+ // second look like it had been honoured.
626
813
  if (result.detectedJavaVersion !== undefined) {
627
- lines.push(`ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${result.detectedJavaVersion}`);
814
+ const willUse = runnableJavaVersion(result.detectedJavaVersion, result.detectedGradleVersion);
815
+ lines.push(`ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${willUse}`);
628
816
  lines.push("");
629
817
  }
630
818
  if (result.warnings.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oFAAoF;IACpF,YAAY,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,8FAA8F;IAC9F,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAiBD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,uBAAuB,CAAC;IACjC,WAAW,EAAE,uBAAuB,CAAC;IACrC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoJD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,aAAa,GAAE,MAAgC,GAAG,MAAM,CAcvF;AAED,wBAAgB,uBAAuB,CACrC,WAAW,GAAE,MAAW,EACxB,KAAK,GAAE,YAAgL,EACvL,QAAQ,GAAE,gBAAmC,EAC7C,eAAe,GAAE,QAAQ,GAAG,KAAgB,EAC5C,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA2NR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA4LR;AAiVD,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,EAC1E,OAAO,GAAE;IACP,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACpC,GACL,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oFAAoF;IACpF,YAAY,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,8FAA8F;IAC9F,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAiBD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,uBAAuB,CAAC;IACjC,WAAW,EAAE,uBAAuB,CAAC;IACrC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoJD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,aAAa,GAAE,MAAgC,GAAG,MAAM,CAcvF;AAED,wBAAgB,uBAAuB,CACrC,WAAW,GAAE,MAAW,EACxB,KAAK,GAAE,YAAgL,EACvL,QAAQ,GAAE,gBAAmC,EAC7C,eAAe,GAAE,QAAQ,GAAG,KAAgB,EAC5C,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA2NR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA4LR;AAiVD,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,EAC1E,OAAO,GAAE;IACP,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACpC,GACL,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -1,7 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import prompts from "prompts";
4
- import { scanAndroidProject, formatScanResult } from "./android-scanner.js";
4
+ import { scanAndroidProject, formatScanResult, runnableJavaVersion, javaVersionProblem, } from "./android-scanner.js";
5
5
  import { scanIosProject, formatIosScanResult } from "./ios-scanner.js";
6
6
  import { SecretsManager } from "../yaml/secrets-manager.js";
7
7
  import { collectFileSecret } from "./file-secret-collector.js";
@@ -1064,10 +1064,10 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
1064
1064
  }
1065
1065
  }
1066
1066
  // 7. Generate and write the pipeline
1067
- // Source/target compatibility (e.g. VERSION_11) doesn't mean the build needs
1068
- // that exact JDK — a newer JDK can cross-compile. Enforce a minimum of 17
1069
- // since ubuntu-latest runners ship with JDK 17+ and JDK <17 is unavailable.
1070
- const javaVersion = Math.max(scanResult.detectedJavaVersion ?? 17, 17);
1067
+ const javaVersion = runnableJavaVersion(scanResult.detectedJavaVersion, scanResult.detectedGradleVersion);
1068
+ const javaProblem = javaVersionProblem(scanResult.detectedJavaVersion, scanResult.detectedGradleVersion);
1069
+ if (javaProblem)
1070
+ console.log(`\n⚠️ ${javaProblem}`);
1071
1071
  const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants, cacheTechnology, options.metaNamespace, detectDefaultBranch());
1072
1072
  writeFileSync(outputPath, yaml, "utf-8");
1073
1073
  console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A cache key that cannot be computed costs a cache, not a pipeline.
3
+ *
4
+ * `cache_key: pods-{{checksum "Podfile.lock"}}` in a repository with no
5
+ * Podfile.lock used to abort conversion, so the build died at `cache-pull`
6
+ * before compiling a single file — including in repositories that use Swift
7
+ * Package Manager and have no reason to own a CocoaPods lockfile at all.
8
+ * Every other step keeps the hard failure: a checksum in a real input is a
9
+ * value that step needs.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=cache-key-missing-file.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key-missing-file.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/cache-key-missing-file.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}