@invarn/cibuild 2.6.4 → 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.
- package/dist/cli.cjs +10 -10
- package/dist/src/commands/android-gms-guard.test.d.ts +9 -0
- package/dist/src/commands/android-gms-guard.test.d.ts.map +1 -0
- package/dist/src/commands/android-gms-guard.test.js +78 -0
- package/dist/src/commands/android-java-version.test.d.ts +12 -0
- package/dist/src/commands/android-java-version.test.d.ts.map +1 -0
- package/dist/src/commands/android-java-version.test.js +85 -0
- package/dist/src/commands/android-scanner.d.ts +68 -0
- package/dist/src/commands/android-scanner.d.ts.map +1 -1
- package/dist/src/commands/android-scanner.js +168 -7
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +5 -5
- package/dist/src/yaml/cache-key-missing-file.test.d.ts +12 -0
- package/dist/src/yaml/cache-key-missing-file.test.d.ts.map +1 -0
- package/dist/src/yaml/cache-key-missing-file.test.js +116 -0
- package/dist/src/yaml/converter.d.ts.map +1 -1
- package/dist/src/yaml/converter.js +32 -2
- package/dist/src/yaml/env-resolver.d.ts +13 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +28 -8
- package/dist/src/yaml/preflight-validation.d.ts +12 -23
- package/dist/src/yaml/preflight-validation.d.ts.map +1 -1
- package/dist/src/yaml/preflight-validation.js +11 -142
- package/dist/src/yaml/steps/android.d.ts.map +1 -1
- package/dist/src/yaml/steps/android.js +17 -1
- package/dist/src/yaml/steps/git-clone.d.ts +8 -2
- package/dist/src/yaml/steps/git-clone.d.ts.map +1 -1
- package/dist/src/yaml/steps/git-clone.js +8 -2
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +9 -5
- package/dist/src/yaml/steps/steps.test.js +32 -0
- package/package.json +1 -1
|
@@ -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;
|
|
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
|
-
/**
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
/id\s*["']com\.google\.gms\.google-services["']/.test(
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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"}
|