@invarn/cibuild 2.7.3 → 2.7.4
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/build.d.ts +21 -0
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +84 -7
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +30 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +144 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BuildWarning } from "./android-scanner.js";
|
|
1
2
|
export interface SetupOptions {
|
|
2
3
|
keystore: boolean;
|
|
3
4
|
keystoreProperties: boolean;
|
|
@@ -8,6 +9,15 @@ export interface SetupOptions {
|
|
|
8
9
|
artifactType: 'apk' | 'aab';
|
|
9
10
|
/** Resolved keystore file paths per workflow (relative to project root), if found on disk. */
|
|
10
11
|
keystorePaths: Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a build file applies the Google Secrets Gradle Plugin and reads a
|
|
14
|
+
* properties file the repository does not carry. The plugin's evaluation
|
|
15
|
+
* listener runs for **every** variant, so a debug build needs the file
|
|
16
|
+
* exactly as much as a release one does.
|
|
17
|
+
*/
|
|
18
|
+
secretsProperties: boolean;
|
|
19
|
+
/** The properties file that plugin reads — `local.properties` unless configured. */
|
|
20
|
+
secretsPropertiesPath: string;
|
|
11
21
|
}
|
|
12
22
|
export interface WorkflowVariantConfig {
|
|
13
23
|
/** Value for the VARIANT env var, e.g. "debug", "release", "stagingDebug". */
|
|
@@ -38,6 +48,17 @@ export interface IosSetupOptions {
|
|
|
38
48
|
appStoreDeploy: boolean;
|
|
39
49
|
bundleId: string;
|
|
40
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The properties file the Secrets Gradle Plugin reads, out of the warnings the
|
|
53
|
+
* scanner produced for it.
|
|
54
|
+
*
|
|
55
|
+
* The two `secrets-plugin` warnings disagree about `location`, and reading the
|
|
56
|
+
* wrong one aims the `file@` step at a build file. The per-key warning carries
|
|
57
|
+
* the **properties file** there; the fallback warning carries the *build file
|
|
58
|
+
* that applies the plugin* and names the properties file only in its message.
|
|
59
|
+
*/
|
|
60
|
+
export declare function secretsPropertiesFileFrom(warnings: BuildWarning[]): string | null;
|
|
61
|
+
export declare function detectSetupNeeds(warnings: BuildWarning[]): Omit<SetupOptions, "keystorePaths">;
|
|
41
62
|
/**
|
|
42
63
|
* The `trigger_map` block appended to every generated pipeline.
|
|
43
64
|
*
|
|
@@ -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":"AASA,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;AAQrE,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;IACtC;;;;;OAKG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oFAAoF;IACpF,qBAAqB,EAAE,MAAM,CAAC;CAC/B;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;AAiID;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,GAAG,IAAI,CAYjF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,CAoB9F;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,aAAa,GAAE,MAAgC,GAAG,MAAM,CAcvF;AAED,wBAAgB,uBAAuB,CACrC,WAAW,GAAE,MAAW,EACxB,KAAK,GAAE,YAAqP,EAC5P,QAAQ,GAAE,gBAAmC,EAC7C,eAAe,GAAE,QAAQ,GAAG,KAAgB,EAC5C,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CAmPR;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"}
|
|
@@ -112,8 +112,34 @@ async function promptVariant(workflowLabel, defaultName, detectedVariants) {
|
|
|
112
112
|
}
|
|
113
113
|
return variantFromName(choice);
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
/**
|
|
116
|
+
* The properties file the Secrets Gradle Plugin reads, out of the warnings the
|
|
117
|
+
* scanner produced for it.
|
|
118
|
+
*
|
|
119
|
+
* The two `secrets-plugin` warnings disagree about `location`, and reading the
|
|
120
|
+
* wrong one aims the `file@` step at a build file. The per-key warning carries
|
|
121
|
+
* the **properties file** there; the fallback warning carries the *build file
|
|
122
|
+
* that applies the plugin* and names the properties file only in its message.
|
|
123
|
+
*/
|
|
124
|
+
export function secretsPropertiesFileFrom(warnings) {
|
|
125
|
+
const plugin = warnings.filter((w) => w.category === "secrets-plugin");
|
|
126
|
+
if (plugin.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
const fromLocation = plugin.find((w) => w.location?.endsWith(".properties"));
|
|
129
|
+
if (fromLocation?.location)
|
|
130
|
+
return fromLocation.location;
|
|
131
|
+
for (const w of plugin) {
|
|
132
|
+
const named = /reads '([^']+)'/.exec(w.message);
|
|
133
|
+
if (named)
|
|
134
|
+
return named[1];
|
|
135
|
+
}
|
|
136
|
+
return "local.properties";
|
|
137
|
+
}
|
|
138
|
+
export function detectSetupNeeds(warnings) {
|
|
139
|
+
const secretsPropertiesPath = secretsPropertiesFileFrom(warnings);
|
|
116
140
|
return {
|
|
141
|
+
secretsProperties: secretsPropertiesPath !== null,
|
|
142
|
+
secretsPropertiesPath: secretsPropertiesPath ?? "local.properties",
|
|
117
143
|
keystore: warnings.some((w) => w.category === "missing-file" && /keystore/i.test(w.message)),
|
|
118
144
|
keystoreProperties: warnings.some((w) => w.category === "missing-file" && /keystore\.properties/i.test(w.message)),
|
|
119
145
|
googleServices: warnings.some((w) => w.category === "firebase" ||
|
|
@@ -148,7 +174,7 @@ trigger_map:
|
|
|
148
174
|
workflow: pull-request
|
|
149
175
|
`;
|
|
150
176
|
}
|
|
151
|
-
export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle", metaNamespace = 'cibuild.io', defaultBranch = FALLBACK_DEFAULT_BRANCH) {
|
|
177
|
+
export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {}, secretsProperties: false, secretsPropertiesPath: 'local.properties' }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle", metaNamespace = 'cibuild.io', defaultBranch = FALLBACK_DEFAULT_BRANCH) {
|
|
152
178
|
const keystoreStepFor = (wf) => {
|
|
153
179
|
if (!setup.keystore)
|
|
154
180
|
return "";
|
|
@@ -178,6 +204,29 @@ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: fa
|
|
|
178
204
|
const googlePlayEnv = setup.googlePlayDeploy
|
|
179
205
|
? ` - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: "" # Set via: ci secrets add GOOGLE_PLAY_SERVICE_ACCOUNT_JSON -w release\n`
|
|
180
206
|
: "";
|
|
207
|
+
// Declared globally and on purpose: `unsetEnvKeys` — what `invarn setup`
|
|
208
|
+
// searches the developer's machine by — reads `envs`, never step inputs. An
|
|
209
|
+
// undeclared LOCAL_PROPERTIES is never discovered and never offered for
|
|
210
|
+
// upload, however many steps name it.
|
|
211
|
+
const secretsPropertiesEnv = setup.secretsProperties
|
|
212
|
+
? ` - LOCAL_PROPERTIES: "" # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES\n`
|
|
213
|
+
: "";
|
|
214
|
+
/**
|
|
215
|
+
* The Secrets Gradle Plugin's properties file, written into every workflow
|
|
216
|
+
* that builds. Its evaluation listener runs for every variant, so a debug
|
|
217
|
+
* build fails to *configure* without it — the error names a module of the
|
|
218
|
+
* user's own project and nothing connects it to a missing credential.
|
|
219
|
+
*/
|
|
220
|
+
const secretsPropertiesStep = setup.secretsProperties
|
|
221
|
+
? `
|
|
222
|
+
- file@1.0.0:
|
|
223
|
+
title: Setup ${setup.secretsPropertiesPath}
|
|
224
|
+
is_skippable: true
|
|
225
|
+
inputs:
|
|
226
|
+
target_path: ${setup.secretsPropertiesPath}
|
|
227
|
+
var_name: LOCAL_PROPERTIES
|
|
228
|
+
`
|
|
229
|
+
: "";
|
|
181
230
|
const googleServicesStep = setup.googleServices
|
|
182
231
|
? `
|
|
183
232
|
- file@1.0.0:
|
|
@@ -241,14 +290,14 @@ app:
|
|
|
241
290
|
- MODULE: app
|
|
242
291
|
- BUILD_TYPE: Debug
|
|
243
292
|
- GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true
|
|
244
|
-
${keystoreEnv}${keystorePropertiesEnv}${googleServicesEnv}${googlePlayEnv}
|
|
293
|
+
${keystoreEnv}${keystorePropertiesEnv}${googleServicesEnv}${googlePlayEnv}${secretsPropertiesEnv}
|
|
245
294
|
workflows:
|
|
246
295
|
primary:
|
|
247
296
|
envs:
|
|
248
297
|
- VARIANT: ${pv.variant}
|
|
249
298
|
${buildTypeEnvOverride(pv)}
|
|
250
299
|
steps:
|
|
251
|
-
${keystoreStepFor("primary")}${googleServicesStep}
|
|
300
|
+
${keystoreStepFor("primary")}${googleServicesStep}${secretsPropertiesStep}
|
|
252
301
|
- set-java-version@1.0.0:
|
|
253
302
|
inputs:
|
|
254
303
|
java_version: '${javaVersion}'
|
|
@@ -288,7 +337,7 @@ ${keystoreStepFor("primary")}${googleServicesStep}
|
|
|
288
337
|
- VARIANT: ${prv.variant}
|
|
289
338
|
${buildTypeEnvOverride(prv)}
|
|
290
339
|
steps:
|
|
291
|
-
${keystoreStepFor("pull-request")}${googleServicesStep}
|
|
340
|
+
${keystoreStepFor("pull-request")}${googleServicesStep}${secretsPropertiesStep}
|
|
292
341
|
- set-java-version@1.0.0:
|
|
293
342
|
inputs:
|
|
294
343
|
java_version: '${javaVersion}'
|
|
@@ -327,7 +376,7 @@ ${keystoreStepFor("pull-request")}${googleServicesStep}
|
|
|
327
376
|
- VARIANT: ${rv.variant}
|
|
328
377
|
${buildTypeEnvOverride(rv)}
|
|
329
378
|
steps:
|
|
330
|
-
${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStep}
|
|
379
|
+
${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStep}${secretsPropertiesStep}
|
|
331
380
|
- set-java-version@1.0.0:
|
|
332
381
|
inputs:
|
|
333
382
|
java_version: '${javaVersion}'
|
|
@@ -894,7 +943,7 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
894
943
|
}
|
|
895
944
|
// 4. Prompt to generate setup steps for detected file issues
|
|
896
945
|
const setupNeeds = detectSetupNeeds(scanResult.warnings);
|
|
897
|
-
const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} };
|
|
946
|
+
const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {}, secretsProperties: false, secretsPropertiesPath: setupNeeds.secretsPropertiesPath };
|
|
898
947
|
const secretsManager = new SecretsManager();
|
|
899
948
|
const keystoreWorkflowGroups = [
|
|
900
949
|
{ label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
|
|
@@ -998,6 +1047,34 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
998
1047
|
}, cwd, secretsManager, ni);
|
|
999
1048
|
}
|
|
1000
1049
|
}
|
|
1050
|
+
if (setupNeeds.secretsProperties) {
|
|
1051
|
+
const propertiesFile = setupNeeds.secretsPropertiesPath;
|
|
1052
|
+
if (ni) {
|
|
1053
|
+
setupOptions.secretsProperties = true;
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
const { create } = await prompts({
|
|
1057
|
+
type: "confirm",
|
|
1058
|
+
name: "create",
|
|
1059
|
+
message: `Generate a setup step for '${propertiesFile}' (read by the Secrets Gradle Plugin)?`,
|
|
1060
|
+
initial: true,
|
|
1061
|
+
});
|
|
1062
|
+
setupOptions.secretsProperties = !!create;
|
|
1063
|
+
}
|
|
1064
|
+
if (setupOptions.secretsProperties) {
|
|
1065
|
+
// Every workflow, not just release: the plugin's evaluation listener
|
|
1066
|
+
// runs for every variant, so a debug build cannot configure without it.
|
|
1067
|
+
await collectFileSecret({
|
|
1068
|
+
displayName: propertiesFile,
|
|
1069
|
+
secretName: "LOCAL_PROPERTIES",
|
|
1070
|
+
fileMatch: (name) => name === propertiesFile.split("/").pop(),
|
|
1071
|
+
readAs: "text",
|
|
1072
|
+
workflowGroups: [
|
|
1073
|
+
{ label: "every workflow", workflows: ["primary", "pull-request", "release"] },
|
|
1074
|
+
],
|
|
1075
|
+
}, cwd, secretsManager, ni);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1001
1078
|
// 4b. Prompt for Google Play deployment setup — skip in non-interactive mode (requires package name + credentials)
|
|
1002
1079
|
if (!ni) {
|
|
1003
1080
|
const { setupGooglePlay } = await prompts({
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A properties file the Secrets Gradle Plugin reads is a credential like any
|
|
3
|
+
* other, and it has to reach every workflow that builds — not just `release`.
|
|
4
|
+
*
|
|
5
|
+
* The scanner has always found this one: it emits a `secrets-plugin` warning
|
|
6
|
+
* whenever a build file applies the plugin and the properties file it reads is
|
|
7
|
+
* gitignored, and `ci init` prints *"The file is gitignored — inject it as a
|
|
8
|
+
* file step before building"* along with the exact step to write. Then it
|
|
9
|
+
* generates a pipeline without that step. The advice was correct and nobody
|
|
10
|
+
* acted on it, so the pipeline it wrote could not configure Gradle at all.
|
|
11
|
+
*
|
|
12
|
+
* Measured, not supposed: the first build in the record to consume an uploaded
|
|
13
|
+
* credential (`bld_f5167b731b95974e61350390`, 2026-09-09) got past
|
|
14
|
+
* google-services and died four steps later on
|
|
15
|
+
*
|
|
16
|
+
* A problem occurred configuring project ':analytics'.
|
|
17
|
+
* > The file '<repo>/local.properties' could not be found
|
|
18
|
+
*
|
|
19
|
+
* on a repository whose `release` workflow had a `file@` step for it and whose
|
|
20
|
+
* `primary` and `pull-request` did not. The plugin's evaluation listener runs
|
|
21
|
+
* for every variant, so a debug build needs the file exactly as much as a
|
|
22
|
+
* release one does.
|
|
23
|
+
*
|
|
24
|
+
* Declaring the slot matters as much as the step. `unsetEnvKeys` — what
|
|
25
|
+
* `invarn setup` searches by — reads `envs`, not step inputs, so an undeclared
|
|
26
|
+
* `LOCAL_PROPERTIES` is never discovered on the developer's machine and never
|
|
27
|
+
* offered for upload, however many steps name it.
|
|
28
|
+
*/
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=local-properties-reaches-every-workflow.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-properties-reaches-every-workflow.test.d.ts","sourceRoot":"","sources":["../../../src/commands/local-properties-reaches-every-workflow.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A properties file the Secrets Gradle Plugin reads is a credential like any
|
|
3
|
+
* other, and it has to reach every workflow that builds — not just `release`.
|
|
4
|
+
*
|
|
5
|
+
* The scanner has always found this one: it emits a `secrets-plugin` warning
|
|
6
|
+
* whenever a build file applies the plugin and the properties file it reads is
|
|
7
|
+
* gitignored, and `ci init` prints *"The file is gitignored — inject it as a
|
|
8
|
+
* file step before building"* along with the exact step to write. Then it
|
|
9
|
+
* generates a pipeline without that step. The advice was correct and nobody
|
|
10
|
+
* acted on it, so the pipeline it wrote could not configure Gradle at all.
|
|
11
|
+
*
|
|
12
|
+
* Measured, not supposed: the first build in the record to consume an uploaded
|
|
13
|
+
* credential (`bld_f5167b731b95974e61350390`, 2026-09-09) got past
|
|
14
|
+
* google-services and died four steps later on
|
|
15
|
+
*
|
|
16
|
+
* A problem occurred configuring project ':analytics'.
|
|
17
|
+
* > The file '<repo>/local.properties' could not be found
|
|
18
|
+
*
|
|
19
|
+
* on a repository whose `release` workflow had a `file@` step for it and whose
|
|
20
|
+
* `primary` and `pull-request` did not. The plugin's evaluation listener runs
|
|
21
|
+
* for every variant, so a debug build needs the file exactly as much as a
|
|
22
|
+
* release one does.
|
|
23
|
+
*
|
|
24
|
+
* Declaring the slot matters as much as the step. `unsetEnvKeys` — what
|
|
25
|
+
* `invarn setup` searches by — reads `envs`, not step inputs, so an undeclared
|
|
26
|
+
* `LOCAL_PROPERTIES` is never discovered on the developer's machine and never
|
|
27
|
+
* offered for upload, however many steps name it.
|
|
28
|
+
*/
|
|
29
|
+
import { describe, test, expect } from '@jest/globals';
|
|
30
|
+
import { load } from 'js-yaml';
|
|
31
|
+
import { generateAndroidPipeline, detectSetupNeeds } from './build.js';
|
|
32
|
+
const BASE = {
|
|
33
|
+
keystore: false,
|
|
34
|
+
keystoreProperties: false,
|
|
35
|
+
googleServices: false,
|
|
36
|
+
googlePlayDeploy: false,
|
|
37
|
+
googlePlayPackageName: '',
|
|
38
|
+
artifactType: 'apk',
|
|
39
|
+
keystorePaths: {},
|
|
40
|
+
secretsProperties: false,
|
|
41
|
+
secretsPropertiesPath: 'local.properties',
|
|
42
|
+
};
|
|
43
|
+
function parse(yaml) {
|
|
44
|
+
return load(yaml);
|
|
45
|
+
}
|
|
46
|
+
function envs(parsed) {
|
|
47
|
+
return Object.assign({}, ...(parsed.app?.envs ?? []));
|
|
48
|
+
}
|
|
49
|
+
function stepNames(parsed, workflow) {
|
|
50
|
+
return (parsed.workflows?.[workflow]?.steps ?? []).map((s) => Object.keys(s)[0]);
|
|
51
|
+
}
|
|
52
|
+
function fileStepsOf(parsed, workflow) {
|
|
53
|
+
return (parsed.workflows?.[workflow]?.steps ?? [])
|
|
54
|
+
.filter((s) => Object.keys(s)[0] === 'file@1.0.0')
|
|
55
|
+
.map((s) => s['file@1.0.0']);
|
|
56
|
+
}
|
|
57
|
+
describe('a properties file the build reads at configuration time', () => {
|
|
58
|
+
test('is declared once and written in every workflow that builds', () => {
|
|
59
|
+
const parsed = parse(generateAndroidPipeline(17, { ...BASE, secretsProperties: true }));
|
|
60
|
+
expect(envs(parsed).LOCAL_PROPERTIES).toBe('');
|
|
61
|
+
// One object per workflow so a failure names the workflow that lost it.
|
|
62
|
+
const written = ['primary', 'pull-request', 'release'].map((workflow) => {
|
|
63
|
+
const step = fileStepsOf(parsed, workflow).find((s) => s.inputs.var_name === 'LOCAL_PROPERTIES');
|
|
64
|
+
return {
|
|
65
|
+
workflow,
|
|
66
|
+
target: step?.inputs.target_path,
|
|
67
|
+
// Text, not binary: re-encoding a properties file as base64 would
|
|
68
|
+
// write base64 to disk and the plugin would read gibberish.
|
|
69
|
+
base64: step?.inputs.base64_encoded,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
expect(written).toEqual([
|
|
73
|
+
{ workflow: 'primary', target: 'local.properties', base64: undefined },
|
|
74
|
+
{ workflow: 'pull-request', target: 'local.properties', base64: undefined },
|
|
75
|
+
{ workflow: 'release', target: 'local.properties', base64: undefined },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
test('is written before anything that could read it', () => {
|
|
79
|
+
const parsed = parse(generateAndroidPipeline(17, { ...BASE, secretsProperties: true }));
|
|
80
|
+
const order = ['primary', 'pull-request', 'release'].map((workflow) => {
|
|
81
|
+
const names = stepNames(parsed, workflow);
|
|
82
|
+
const wrote = names.indexOf('file@1.0.0');
|
|
83
|
+
const firstBuildTool = names.findIndex((n) => /^(gradle-build|android-lint|android-unit-test)@/.test(n));
|
|
84
|
+
return { workflow, writesBeforeBuilding: wrote >= 0 && firstBuildTool > wrote };
|
|
85
|
+
});
|
|
86
|
+
expect(order).toEqual([
|
|
87
|
+
{ workflow: 'primary', writesBeforeBuilding: true },
|
|
88
|
+
{ workflow: 'pull-request', writesBeforeBuilding: true },
|
|
89
|
+
{ workflow: 'release', writesBeforeBuilding: true },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
test('follows the plugin to a properties file that is not local.properties', () => {
|
|
93
|
+
const parsed = parse(generateAndroidPipeline(17, {
|
|
94
|
+
...BASE,
|
|
95
|
+
secretsProperties: true,
|
|
96
|
+
secretsPropertiesPath: 'config/secrets.properties',
|
|
97
|
+
}));
|
|
98
|
+
const written = fileStepsOf(parsed, 'primary').find((s) => s.inputs.var_name === 'LOCAL_PROPERTIES');
|
|
99
|
+
expect(written.inputs.target_path).toBe('config/secrets.properties');
|
|
100
|
+
});
|
|
101
|
+
test('appears nowhere in a repository that does not use the plugin', () => {
|
|
102
|
+
// The whole point of scanning: a pipeline must not declare a slot the
|
|
103
|
+
// build never reads. After issue 03 a declared-and-empty slot fails the
|
|
104
|
+
// build, so an unnecessary one turns a working repository red.
|
|
105
|
+
const yaml = generateAndroidPipeline(17, BASE);
|
|
106
|
+
expect(yaml).not.toContain('LOCAL_PROPERTIES');
|
|
107
|
+
expect(fileStepsOf(parse(yaml), 'primary')).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('the scan that decides it', () => {
|
|
111
|
+
const warning = (over) => ({
|
|
112
|
+
category: 'secrets-plugin',
|
|
113
|
+
severity: 'warning',
|
|
114
|
+
message: 'Secrets plugin key: MAPS_API_KEY',
|
|
115
|
+
location: 'local.properties',
|
|
116
|
+
...over,
|
|
117
|
+
});
|
|
118
|
+
test('takes the properties file from the per-key warning', () => {
|
|
119
|
+
const needs = detectSetupNeeds([warning({})]);
|
|
120
|
+
expect(needs.secretsProperties).toBe(true);
|
|
121
|
+
expect(needs.secretsPropertiesPath).toBe('local.properties');
|
|
122
|
+
});
|
|
123
|
+
test('takes it from the message when the warning has no defaults file', () => {
|
|
124
|
+
// The two `secrets-plugin` warnings disagree about `location`: the
|
|
125
|
+
// per-key one carries the properties file, the fallback one carries the
|
|
126
|
+
// *build file* that applies the plugin. Reading the wrong one would point
|
|
127
|
+
// the step at `app/build.gradle.kts`.
|
|
128
|
+
const needs = detectSetupNeeds([
|
|
129
|
+
warning({
|
|
130
|
+
message: "Google Secrets Gradle Plugin reads 'config/secrets.properties' which is absent in CI",
|
|
131
|
+
location: 'app/build.gradle.kts',
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
expect(needs.secretsProperties).toBe(true);
|
|
135
|
+
expect(needs.secretsPropertiesPath).toBe('config/secrets.properties');
|
|
136
|
+
});
|
|
137
|
+
test('is off when nothing reports the plugin', () => {
|
|
138
|
+
const needs = detectSetupNeeds([
|
|
139
|
+
{ category: 'missing-file', severity: 'warning', message: 'keystore.properties not found' },
|
|
140
|
+
]);
|
|
141
|
+
expect(needs.secretsProperties).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
//# sourceMappingURL=local-properties-reaches-every-workflow.test.js.map
|