@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.
@@ -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":"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
+ {"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
- function detectSetupNeeds(warnings) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.7.3",
3
+ "version": "2.7.4",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",