@invarn/cibuild 2.6.5 → 2.6.6

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,85 @@
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('raises a declared version to the oldest installed JDK that covers it', () => {
49
+ expect(runnableJavaVersion(11)).toBe(17);
50
+ expect(runnableJavaVersion(17)).toBe(17);
51
+ expect(runnableJavaVersion(21)).toBe(21);
52
+ });
53
+ test('defaults to the oldest installed JDK when nothing is declared', () => {
54
+ expect(runnableJavaVersion(undefined)).toBe(17);
55
+ });
56
+ test('never names a JDK no build machine has', () => {
57
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(23));
58
+ expect(AVAILABLE_JAVA_VERSIONS).toContain(runnableJavaVersion(25, '8.9'));
59
+ });
60
+ test('lets an old wrapper cap a newer declaration', () => {
61
+ // Gradle 7.x tops out at 17 even though 21 is installed.
62
+ expect(runnableJavaVersion(21, '7.6.1')).toBe(17);
63
+ });
64
+ test('honours the Gradle 9 floor of 17', () => {
65
+ expect(runnableJavaVersion(8, '9.0')).toBe(17);
66
+ });
67
+ });
68
+ describe('javaVersionProblem', () => {
69
+ test('names a Gradle version no installed JDK can run', () => {
70
+ const problem = javaVersionProblem(undefined, '5.2.1');
71
+ expect(problem).toContain('Gradle 5.2.1');
72
+ expect(problem).toContain('Java 11');
73
+ });
74
+ test('names a declaration above every installed JDK', () => {
75
+ const problem = javaVersionProblem(23, '8.9');
76
+ expect(problem).toContain('Java 23');
77
+ expect(problem).toContain('17 and 21');
78
+ });
79
+ test('is silent when every constraint was honoured', () => {
80
+ expect(javaVersionProblem(17, '8.9')).toBeUndefined();
81
+ expect(javaVersionProblem(undefined, '8.9')).toBeUndefined();
82
+ expect(javaVersionProblem(undefined, undefined)).toBeUndefined();
83
+ });
84
+ });
85
+ //# 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,72 @@ 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
+ export declare const AVAILABLE_JAVA_VERSIONS: number[];
72
+ /**
73
+ * The Java version to write into a generated pipeline: the oldest available
74
+ * JDK that satisfies both what the sources declare and what the Gradle
75
+ * wrapper can run on.
76
+ *
77
+ * Source compatibility is a floor, not an exact request — a project
78
+ * declaring 11 compiles fine on 17 — but it is not the only constraint.
79
+ * Gradle starts before it reads a single `sourceCompatibility`, so a wrapper
80
+ * pinning an old Gradle caps the JDK no matter what the sources say, and a
81
+ * declaration above every installed JDK cannot be honoured at all.
82
+ *
83
+ * Falls back to the lowest available JDK when the constraints cannot all be
84
+ * met, because a pipeline has to name something; `javaVersionProblem` says
85
+ * what was given up so the caller can warn.
86
+ */
87
+ export declare function runnableJavaVersion(detectedJavaVersion: number | undefined, gradleVersion?: string): number;
88
+ /**
89
+ * What `runnableJavaVersion` could not honour, as a sentence, or undefined
90
+ * when it honoured everything.
91
+ */
92
+ export declare function javaVersionProblem(detectedJavaVersion: number | undefined, gradleVersion?: string): string | undefined;
25
93
  /**
26
94
  * Detects build types and product flavors from Gradle files by static parsing.
27
95
  * 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;;;GAGG;AACH,eAAO,MAAM,uBAAuB,UAAW,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAQR;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAYpB;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,99 @@ 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
+ export const AVAILABLE_JAVA_VERSIONS = [17, 21];
380
+ /**
381
+ * The Java version to write into a generated pipeline: the oldest available
382
+ * JDK that satisfies both what the sources declare and what the Gradle
383
+ * wrapper can run on.
384
+ *
385
+ * Source compatibility is a floor, not an exact request — a project
386
+ * declaring 11 compiles fine on 17 — but it is not the only constraint.
387
+ * Gradle starts before it reads a single `sourceCompatibility`, so a wrapper
388
+ * pinning an old Gradle caps the JDK no matter what the sources say, and a
389
+ * declaration above every installed JDK cannot be honoured at all.
390
+ *
391
+ * Falls back to the lowest available JDK when the constraints cannot all be
392
+ * met, because a pipeline has to name something; `javaVersionProblem` says
393
+ * what was given up so the caller can warn.
394
+ */
395
+ export function runnableJavaVersion(detectedJavaVersion, gradleVersion) {
396
+ const lowest = Math.min(...AVAILABLE_JAVA_VERSIONS);
397
+ const ceiling = gradleJavaCeiling(gradleVersion) ?? Infinity;
398
+ const floor = Math.max(detectedJavaVersion ?? 0, gradleJavaFloor(gradleVersion) ?? 0);
399
+ const usable = AVAILABLE_JAVA_VERSIONS.filter((v) => v >= floor && v <= ceiling).sort((a, b) => a - b);
400
+ return usable.length > 0 ? usable[0] : lowest;
401
+ }
402
+ /**
403
+ * What `runnableJavaVersion` could not honour, as a sentence, or undefined
404
+ * when it honoured everything.
405
+ */
406
+ export function javaVersionProblem(detectedJavaVersion, gradleVersion) {
407
+ const installed = AVAILABLE_JAVA_VERSIONS.join(" and ");
408
+ const ceiling = gradleJavaCeiling(gradleVersion);
409
+ const lowest = Math.min(...AVAILABLE_JAVA_VERSIONS);
410
+ if (ceiling !== undefined && ceiling < lowest) {
411
+ 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.`;
412
+ }
413
+ const highest = Math.max(...AVAILABLE_JAVA_VERSIONS);
414
+ if ((detectedJavaVersion ?? 0) > highest) {
415
+ 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".`;
416
+ }
417
+ return undefined;
418
+ }
263
419
  // ---------------------------------------------------------------------------
264
420
  // Gitignore helper
265
421
  // ---------------------------------------------------------------------------
@@ -399,6 +555,7 @@ export async function scanAndroidProject(projectRoot) {
399
555
  // ------------------------------------------------------------------
400
556
  const gradleFiles = findGradleFiles(projectRoot);
401
557
  const detectedJavaVersion = detectRequiredJavaVersion(gradleFiles);
558
+ const detectedGradleVersion = detectGradleWrapperVersion(projectRoot);
402
559
  const buildVariants = detectBuildVariants(gradleFiles);
403
560
  // ------------------------------------------------------------------
404
561
  // 3. Scan Gradle files
@@ -605,7 +762,7 @@ export async function scanAndroidProject(projectRoot) {
605
762
  });
606
763
  }
607
764
  }
608
- return { warnings, detectedJavaVersion, buildVariants };
765
+ return { warnings, detectedJavaVersion, detectedGradleVersion, buildVariants };
609
766
  }
610
767
  // ---------------------------------------------------------------------------
611
768
  // Formatter
@@ -622,9 +779,13 @@ const CATEGORY_LABELS = {
622
779
  export function formatScanResult(result) {
623
780
  const lines = [];
624
781
  const sep = "─".repeat(50);
625
- // Always show the detected Java version
782
+ // Always show the detected Java version, and the one the pipeline will
783
+ // actually name — they differ whenever the declaration is below the oldest
784
+ // installed JDK or above the newest, and saying only the first made the
785
+ // second look like it had been honoured.
626
786
  if (result.detectedJavaVersion !== undefined) {
627
- lines.push(`ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${result.detectedJavaVersion}`);
787
+ const willUse = runnableJavaVersion(result.detectedJavaVersion, result.detectedGradleVersion);
788
+ lines.push(`ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${willUse}`);
628
789
  lines.push("");
629
790
  }
630
791
  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"}