@invarn/cibuild 2.7.3 → 2.7.5

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.
Files changed (54) hide show
  1. package/dist/cli.cjs +10 -10
  2. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
  3. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
  4. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +219 -0
  5. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
  6. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
  7. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +291 -0
  8. package/dist/src/commands/build.d.ts +144 -0
  9. package/dist/src/commands/build.d.ts.map +1 -1
  10. package/dist/src/commands/build.js +317 -22
  11. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
  12. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
  13. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
  14. package/dist/src/commands/file-secret-collector.d.ts +13 -0
  15. package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
  16. package/dist/src/commands/file-secret-collector.js +3 -2
  17. package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +30 -0
  18. package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +1 -0
  19. package/dist/src/commands/local-properties-reaches-every-workflow.test.js +146 -0
  20. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
  21. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
  22. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
  23. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
  24. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
  25. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
  26. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
  27. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
  28. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
  29. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
  30. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
  31. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
  32. package/dist/src/yaml/converter.d.ts +48 -0
  33. package/dist/src/yaml/converter.d.ts.map +1 -1
  34. package/dist/src/yaml/converter.js +117 -0
  35. package/dist/src/yaml/env-resolver.d.ts +52 -0
  36. package/dist/src/yaml/env-resolver.d.ts.map +1 -1
  37. package/dist/src/yaml/env-resolver.js +99 -1
  38. package/dist/src/yaml/secrets-manager.d.ts +13 -3
  39. package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
  40. package/dist/src/yaml/secrets-manager.js +25 -14
  41. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
  42. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
  43. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
  44. package/dist/src/yaml/steps/index.d.ts.map +1 -1
  45. package/dist/src/yaml/steps/index.js +9 -0
  46. package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
  47. package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
  48. package/dist/src/yaml/steps/ios-deps.js +18 -3
  49. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
  50. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
  51. package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
  52. package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
  53. package/dist/src/yaml/steps/xcode.js +73 -20
  54. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import type { BuildWarning } from "./android-scanner.js";
2
+ import type { WorkflowGroup } from "./file-secret-collector.js";
1
3
  export interface SetupOptions {
2
4
  keystore: boolean;
3
5
  keystoreProperties: boolean;
@@ -8,6 +10,44 @@ export interface SetupOptions {
8
10
  artifactType: 'apk' | 'aab';
9
11
  /** Resolved keystore file paths per workflow (relative to project root), if found on disk. */
10
12
  keystorePaths: Record<string, string>;
13
+ /**
14
+ * Where each workflow's `google-services.json` is written. It follows the
15
+ * **flavor** the workflow builds: Android gives every product flavor its own
16
+ * source set, each with its own copy of the file, and the plugin resolves by
17
+ * specificity — variant, build type, flavor, module root. A workflow
18
+ * building `ciDynamicDebug` therefore wants
19
+ * `app/src/ciDynamic/google-services.json`, and a workflow building
20
+ * `prodRelease` wants a different file at a different path.
21
+ *
22
+ * The *value* varies per workflow too, and that needs no second map: a
23
+ * secret is scoped to a workflow by its name — `CIBUILD_SW__PRIMARY__X`
24
+ * beside `CIBUILD_SW__RELEASE__X` — which `SecretsManager` decodes and
25
+ * `EnvResolver` resolves for the workflow being run. One `var_name` in the
26
+ * step, one declared slot, and the workflow picks the value.
27
+ */
28
+ googleServicesPaths: Record<string, string>;
29
+ /**
30
+ * The product flavor each workflow's variant builds, or null where the
31
+ * variant names none.
32
+ *
33
+ * `LOCAL_PROPERTIES` follows this rather than a target path, and it is the
34
+ * only credential that has to: every workflow writes the same
35
+ * `local.properties` to the same place, so nothing about the path says the
36
+ * bytes differ. What belongs *in* the file is the flavor's environment — a
37
+ * different base API URL plus twenty other keys the Secrets Gradle Plugin
38
+ * turns into `BuildConfig` — so two workflows on different flavors cannot
39
+ * share one value, and `credentialSlots` splits the slot when they disagree.
40
+ */
41
+ workflowFlavors: Record<string, string | null>;
42
+ /**
43
+ * Whether a build file applies the Google Secrets Gradle Plugin and reads a
44
+ * properties file the repository does not carry. The plugin's evaluation
45
+ * listener runs for **every** variant, so a debug build needs the file
46
+ * exactly as much as a release one does.
47
+ */
48
+ secretsProperties: boolean;
49
+ /** The properties file that plugin reads — `local.properties` unless configured. */
50
+ secretsPropertiesPath: string;
11
51
  }
12
52
  export interface WorkflowVariantConfig {
13
53
  /** Value for the VARIANT env var, e.g. "debug", "release", "stagingDebug". */
@@ -38,6 +78,110 @@ export interface IosSetupOptions {
38
78
  appStoreDeploy: boolean;
39
79
  bundleId: string;
40
80
  }
81
+ /**
82
+ * The properties file the Secrets Gradle Plugin reads, out of the warnings the
83
+ * scanner produced for it.
84
+ *
85
+ * The two `secrets-plugin` warnings disagree about `location`, and reading the
86
+ * wrong one aims the `file@` step at a build file. The per-key warning carries
87
+ * the **properties file** there; the fallback warning carries the *build file
88
+ * that applies the plugin* and names the properties file only in its message.
89
+ */
90
+ export declare function secretsPropertiesFileFrom(warnings: BuildWarning[]): string | null;
91
+ /**
92
+ * The product flavor a variant builds, out of the flavors the scanner found —
93
+ * `ciDynamicDebug` is a build of `ciDynamic`, `prodRelease` of `prod`.
94
+ *
95
+ * An Android variant name is the flavor followed by the capitalised build
96
+ * type, so the character after the flavor has to be an upper-case one. That
97
+ * check is load-bearing: without it a flavor called `pro` would claim
98
+ * `prodRelease`. The longest match wins, so `ciDynamic` beats `ci` on a
99
+ * repository that declares both.
100
+ */
101
+ export declare function flavorOfVariant(variant: string, productFlavors: string[]): string | null;
102
+ /** Where a flavor's `google-services.json` belongs inside the app module. */
103
+ export declare function googleServicesPathFor(flavor: string | null, module?: string): string;
104
+ /**
105
+ * The per-workflow path and key maps for `SetupOptions`, from the variants the
106
+ * workflows build and the flavors the scanner found.
107
+ *
108
+ * `app/google-services.json` used to be hardcoded here. For a flavored build
109
+ * that is the plugin's *fallback* location, not the right one, and writing
110
+ * there is silently wrong rather than loudly wrong: the plugin finds a file,
111
+ * parses it, and fails on a package name — which reads as a Firebase
112
+ * misconfiguration rather than as the wrong file having been delivered. That
113
+ * is what `bld_8e60d90ab4e55f85d2f72459` spent 1292 Gradle tasks proving.
114
+ */
115
+ export declare function googleServicesTargets(variants: WorkflowVariants, productFlavors: string[], module?: string): Record<string, string>;
116
+ /** The three workflows every generated pipeline has, in the order they are emitted. */
117
+ export declare const GENERATED_WORKFLOWS: readonly ["primary", "pull-request", "release"];
118
+ /**
119
+ * The `workflowFlavors` map for `SetupOptions`, from the variants the
120
+ * workflows build and the flavors the scanner found.
121
+ */
122
+ export declare function variantFlavors(variants: WorkflowVariants, productFlavors: string[]): Record<string, string | null>;
123
+ /** One `envs` slot to declare, and the workflow it belongs to. */
124
+ export interface CredentialSlot {
125
+ slot: string;
126
+ /** null when one slot serves every workflow that reads the credential. */
127
+ workflow: string | null;
128
+ }
129
+ /**
130
+ * The `envs` slots one credential needs: one when every workflow that reads it
131
+ * wants the same bytes, one per workflow when they do not.
132
+ *
133
+ * What "the same bytes" means is the credential's own business — a keystore
134
+ * follows the target path the pipeline declares for it, `google-services.json`
135
+ * the flavor's source set, `local.properties` the flavor itself — so the
136
+ * caller passes the discriminator it already computed, one entry per workflow
137
+ * that reads the credential, and this decides only whether they agree. A
138
+ * credential only one workflow reads can never disagree, which is why the
139
+ * genuinely release-only ones keep one slot however flavored the repository
140
+ * is.
141
+ *
142
+ * A disagreement is declared under `CIBUILD_SW__<WORKFLOW>__<VAR>`: the name
143
+ * `ci secrets upload` already encodes a workflow-scoped value under, that
144
+ * `SecretsManager` already decodes, and that `EnvResolver` already resolves
145
+ * for the workflow being run. Invarn's secret scopes have no workflow column,
146
+ * but a flat key→value map carries a workflow perfectly well when the workflow
147
+ * is part of the key.
148
+ *
149
+ * Every workflow gets a slot, not only the odd one out. The encoded name is
150
+ * per workflow, so a workflow left without one would have neither a value nor
151
+ * the declaration `file@1.0.0` refuses over.
152
+ *
153
+ * The `file@` step's `var_name` stays unsuffixed either way. The decoding does
154
+ * the mapping; a second naming convention in the step would be a second way to
155
+ * say the same thing.
156
+ */
157
+ export declare function credentialSlots(varName: string, discriminatorByWorkflow: Record<string, string | null | undefined>): CredentialSlot[];
158
+ /** One group of workflows a credential's value is collected for, and what they share. */
159
+ export interface CollectionGroup extends WorkflowGroup {
160
+ /** The value of whatever the credential follows, shared by this group's workflows. */
161
+ discriminator: string;
162
+ }
163
+ /**
164
+ * The groups `ci init` collects a credential's value in: one per distinct value
165
+ * of whatever that credential follows, in workflow order.
166
+ *
167
+ * The same discriminator `credentialSlots` splits the *declaration* by, and
168
+ * deliberately the same function computing it, so what `ci init` asks for and
169
+ * what the pipeline declares cannot drift apart. One group means one slot and
170
+ * one question; two groups mean one slot per workflow and a question per group.
171
+ */
172
+ export declare function collectionGroups(discriminatorByWorkflow: Record<string, string>, label: (discriminator: string, workflows: string[]) => string): CollectionGroup[];
173
+ /**
174
+ * The groups the Secrets Gradle Plugin's properties file is collected in: one
175
+ * per flavor.
176
+ *
177
+ * Unlike `google-services.json`, nothing about this credential's target says
178
+ * the bytes differ — every workflow writes the same file to the same path.
179
+ * What differs is the content: the plugin turns the file into `BuildConfig`,
180
+ * and a flavor is an environment. Asking once and storing one value gives
181
+ * `prod` the CI backend's base URL, on a green build.
182
+ */
183
+ export declare function propertiesFileGroups(workflowFlavors: Record<string, string | null>): CollectionGroup[];
184
+ export declare function detectSetupNeeds(warnings: BuildWarning[]): Omit<SetupOptions, "keystorePaths" | "googleServicesPaths" | "workflowFlavors">;
41
185
  /**
42
186
  * The `trigger_map` block appended to every generated pipeline.
43
187
  *
@@ -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":"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"}
@@ -4,6 +4,7 @@ import prompts from "prompts";
4
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
+ import { encodeSecretName } from "./secrets-upload.js";
7
8
  import { collectFileSecret } from "./file-secret-collector.js";
8
9
  import { generateGitHubActionsWorkflow } from "./github-workflow.js";
9
10
  import { detectDefaultBranch, FALLBACK_DEFAULT_BRANCH } from "../shared/git.js";
@@ -112,8 +113,182 @@ async function promptVariant(workflowLabel, defaultName, detectedVariants) {
112
113
  }
113
114
  return variantFromName(choice);
114
115
  }
115
- function detectSetupNeeds(warnings) {
116
+ /**
117
+ * The properties file the Secrets Gradle Plugin reads, out of the warnings the
118
+ * scanner produced for it.
119
+ *
120
+ * The two `secrets-plugin` warnings disagree about `location`, and reading the
121
+ * wrong one aims the `file@` step at a build file. The per-key warning carries
122
+ * the **properties file** there; the fallback warning carries the *build file
123
+ * that applies the plugin* and names the properties file only in its message.
124
+ */
125
+ export function secretsPropertiesFileFrom(warnings) {
126
+ const plugin = warnings.filter((w) => w.category === "secrets-plugin");
127
+ if (plugin.length === 0)
128
+ return null;
129
+ const fromLocation = plugin.find((w) => w.location?.endsWith(".properties"));
130
+ if (fromLocation?.location)
131
+ return fromLocation.location;
132
+ for (const w of plugin) {
133
+ const named = /reads '([^']+)'/.exec(w.message);
134
+ if (named)
135
+ return named[1];
136
+ }
137
+ return "local.properties";
138
+ }
139
+ /**
140
+ * The product flavor a variant builds, out of the flavors the scanner found —
141
+ * `ciDynamicDebug` is a build of `ciDynamic`, `prodRelease` of `prod`.
142
+ *
143
+ * An Android variant name is the flavor followed by the capitalised build
144
+ * type, so the character after the flavor has to be an upper-case one. That
145
+ * check is load-bearing: without it a flavor called `pro` would claim
146
+ * `prodRelease`. The longest match wins, so `ciDynamic` beats `ci` on a
147
+ * repository that declares both.
148
+ */
149
+ export function flavorOfVariant(variant, productFlavors) {
150
+ const matches = productFlavors
151
+ .filter((flavor) => {
152
+ if (flavor.length === 0 || !variant.startsWith(flavor))
153
+ return false;
154
+ const head = variant.slice(flavor.length)[0];
155
+ return head !== undefined && head === head.toUpperCase() && head !== head.toLowerCase();
156
+ })
157
+ .sort((a, b) => b.length - a.length);
158
+ return matches[0] ?? null;
159
+ }
160
+ /** Where a flavor's `google-services.json` belongs inside the app module. */
161
+ export function googleServicesPathFor(flavor, module = "app") {
162
+ return flavor
163
+ ? `${module}/src/${flavor}/google-services.json`
164
+ : `${module}/google-services.json`;
165
+ }
166
+ /**
167
+ * The per-workflow path and key maps for `SetupOptions`, from the variants the
168
+ * workflows build and the flavors the scanner found.
169
+ *
170
+ * `app/google-services.json` used to be hardcoded here. For a flavored build
171
+ * that is the plugin's *fallback* location, not the right one, and writing
172
+ * there is silently wrong rather than loudly wrong: the plugin finds a file,
173
+ * parses it, and fails on a package name — which reads as a Firebase
174
+ * misconfiguration rather than as the wrong file having been delivered. That
175
+ * is what `bld_8e60d90ab4e55f85d2f72459` spent 1292 Gradle tasks proving.
176
+ */
177
+ export function googleServicesTargets(variants, productFlavors, module = "app") {
178
+ const paths = {};
179
+ for (const [workflow, config] of variantByWorkflow(variants)) {
180
+ const flavor = flavorOfVariant(config.variant, productFlavors);
181
+ paths[workflow] = googleServicesPathFor(flavor, module);
182
+ }
183
+ return paths;
184
+ }
185
+ /** The three workflows every generated pipeline has, in the order they are emitted. */
186
+ export const GENERATED_WORKFLOWS = ["primary", "pull-request", "release"];
187
+ /** The variant each workflow builds, keyed by the workflow's name in the YAML. */
188
+ function variantByWorkflow(variants) {
189
+ return [
190
+ ["primary", variants.primary],
191
+ ["pull-request", variants.pullRequest],
192
+ ["release", variants.release],
193
+ ];
194
+ }
195
+ /**
196
+ * The `workflowFlavors` map for `SetupOptions`, from the variants the
197
+ * workflows build and the flavors the scanner found.
198
+ */
199
+ export function variantFlavors(variants, productFlavors) {
200
+ const flavors = {};
201
+ for (const [workflow, config] of variantByWorkflow(variants)) {
202
+ flavors[workflow] = flavorOfVariant(config.variant, productFlavors);
203
+ }
204
+ return flavors;
205
+ }
206
+ /**
207
+ * The `envs` slots one credential needs: one when every workflow that reads it
208
+ * wants the same bytes, one per workflow when they do not.
209
+ *
210
+ * What "the same bytes" means is the credential's own business — a keystore
211
+ * follows the target path the pipeline declares for it, `google-services.json`
212
+ * the flavor's source set, `local.properties` the flavor itself — so the
213
+ * caller passes the discriminator it already computed, one entry per workflow
214
+ * that reads the credential, and this decides only whether they agree. A
215
+ * credential only one workflow reads can never disagree, which is why the
216
+ * genuinely release-only ones keep one slot however flavored the repository
217
+ * is.
218
+ *
219
+ * A disagreement is declared under `CIBUILD_SW__<WORKFLOW>__<VAR>`: the name
220
+ * `ci secrets upload` already encodes a workflow-scoped value under, that
221
+ * `SecretsManager` already decodes, and that `EnvResolver` already resolves
222
+ * for the workflow being run. Invarn's secret scopes have no workflow column,
223
+ * but a flat key→value map carries a workflow perfectly well when the workflow
224
+ * is part of the key.
225
+ *
226
+ * Every workflow gets a slot, not only the odd one out. The encoded name is
227
+ * per workflow, so a workflow left without one would have neither a value nor
228
+ * the declaration `file@1.0.0` refuses over.
229
+ *
230
+ * The `file@` step's `var_name` stays unsuffixed either way. The decoding does
231
+ * the mapping; a second naming convention in the step would be a second way to
232
+ * say the same thing.
233
+ */
234
+ export function credentialSlots(varName, discriminatorByWorkflow) {
235
+ const distinct = new Set(Object.values(discriminatorByWorkflow));
236
+ if (distinct.size <= 1)
237
+ return [{ slot: varName, workflow: null }];
238
+ return Object.keys(discriminatorByWorkflow).map((workflow) => ({
239
+ slot: encodeSecretName(varName, workflow),
240
+ workflow,
241
+ }));
242
+ }
243
+ /**
244
+ * One discriminator per workflow, defaulted the way the step that writes the
245
+ * credential defaults its own input — so a repository whose scan resolved
246
+ * nothing does not read as three workflows that disagree.
247
+ */
248
+ function discriminators(map, fallback) {
249
+ const out = {};
250
+ for (const workflow of GENERATED_WORKFLOWS)
251
+ out[workflow] = map[workflow] ?? fallback;
252
+ return out;
253
+ }
254
+ /**
255
+ * The groups `ci init` collects a credential's value in: one per distinct value
256
+ * of whatever that credential follows, in workflow order.
257
+ *
258
+ * The same discriminator `credentialSlots` splits the *declaration* by, and
259
+ * deliberately the same function computing it, so what `ci init` asks for and
260
+ * what the pipeline declares cannot drift apart. One group means one slot and
261
+ * one question; two groups mean one slot per workflow and a question per group.
262
+ */
263
+ export function collectionGroups(discriminatorByWorkflow, label) {
264
+ const byDiscriminator = new Map();
265
+ for (const [workflow, value] of Object.entries(discriminatorByWorkflow)) {
266
+ byDiscriminator.set(value, [...(byDiscriminator.get(value) ?? []), workflow]);
267
+ }
268
+ return [...byDiscriminator].map(([discriminator, workflows]) => ({
269
+ label: label(discriminator, workflows),
270
+ workflows,
271
+ discriminator,
272
+ }));
273
+ }
274
+ /**
275
+ * The groups the Secrets Gradle Plugin's properties file is collected in: one
276
+ * per flavor.
277
+ *
278
+ * Unlike `google-services.json`, nothing about this credential's target says
279
+ * the bytes differ — every workflow writes the same file to the same path.
280
+ * What differs is the content: the plugin turns the file into `BuildConfig`,
281
+ * and a flavor is an environment. Asking once and storing one value gives
282
+ * `prod` the CI backend's base URL, on a green build.
283
+ */
284
+ export function propertiesFileGroups(workflowFlavors) {
285
+ return collectionGroups(discriminators(workflowFlavors, ""), (flavor, workflows) => flavor ? `the ${flavor} flavor (${workflows.join(", ")})` : "every workflow");
286
+ }
287
+ export function detectSetupNeeds(warnings) {
288
+ const secretsPropertiesPath = secretsPropertiesFileFrom(warnings);
116
289
  return {
290
+ secretsProperties: secretsPropertiesPath !== null,
291
+ secretsPropertiesPath: secretsPropertiesPath ?? "local.properties",
117
292
  keystore: warnings.some((w) => w.category === "missing-file" && /keystore/i.test(w.message)),
118
293
  keystoreProperties: warnings.some((w) => w.category === "missing-file" && /keystore\.properties/i.test(w.message)),
119
294
  googleServices: warnings.some((w) => w.category === "firebase" ||
@@ -148,7 +323,7 @@ trigger_map:
148
323
  workflow: pull-request
149
324
  `;
150
325
  }
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) {
326
+ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {}, googleServicesPaths: {}, workflowFlavors: {}, secretsProperties: false, secretsPropertiesPath: 'local.properties' }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle", metaNamespace = 'cibuild.io', defaultBranch = FALLBACK_DEFAULT_BRANCH) {
152
327
  const keystoreStepFor = (wf) => {
153
328
  if (!setup.keystore)
154
329
  return "";
@@ -165,28 +340,80 @@ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: fa
165
340
  };
166
341
  // Add BUILD_TYPE override to workflow envs when it differs from the global default ("Debug")
167
342
  const buildTypeEnvOverride = (v) => v.buildType !== "Debug" ? ` - BUILD_TYPE: ${v.buildType}\n` : "";
168
- // Optional env var stubs (empty defaults — filled via ci secrets add)
343
+ // Optional env var stubs (empty defaults — filled via ci secrets add).
344
+ //
345
+ // Declared globally and on purpose: `unsetEnvKeys` — what `invarn setup`
346
+ // searches the developer's machine by — reads `envs`, never step inputs. An
347
+ // undeclared credential is never discovered and never offered for upload,
348
+ // however many steps name it.
349
+ //
350
+ // One slot per credential where every workflow that reads it wants the same
351
+ // bytes, and one per workflow where they do not — see `credentialSlots`.
352
+ // The `var_name` in the step below stays unsuffixed either way.
353
+ const declare = (varName, discriminatorByWorkflow, comment) => credentialSlots(varName, discriminatorByWorkflow)
354
+ .map(({ slot, workflow }) => ` - ${slot}: ""${comment(workflow)}\n`)
355
+ .join("");
169
356
  const keystoreEnv = setup.keystore
170
- ? ` - KEYSTORE_BASE64: "" # Set via: ci secrets add KEYSTORE_BASE64 -w primary\n`
357
+ ? declare("KEYSTORE_BASE64", discriminators(setup.keystorePaths, "keystore.jks"), (wf) => wf
358
+ ? ` # Set via: ci secrets add KEYSTORE_BASE64 -w ${wf}`
359
+ : ` # Set via: ci secrets add KEYSTORE_BASE64 -w primary`)
171
360
  : "";
361
+ // Release-only, and legitimately so: nothing but `release` signs a release
362
+ // or talks to Google Play, so there is no second workflow to disagree with.
172
363
  const keystorePropertiesEnv = setup.keystoreProperties
173
364
  ? ` - KEYSTORE_PROPERTIES: "" # Release signing — set via: ci secrets add KEYSTORE_PROPERTIES -w release\n`
174
365
  : "";
175
366
  const googleServicesEnv = setup.googleServices
176
- ? ` - GOOGLE_SERVICES_JSON: "" # Set via: ci secrets add GOOGLE_SERVICES_JSON\n`
367
+ ? declare("GOOGLE_SERVICES_JSON", discriminators(setup.googleServicesPaths, "app/google-services.json"), (wf) => wf
368
+ ? ` # Set via: ci secrets add GOOGLE_SERVICES_JSON -w ${wf}`
369
+ : ` # Set via: ci secrets add GOOGLE_SERVICES_JSON`)
177
370
  : "";
178
371
  const googlePlayEnv = setup.googlePlayDeploy
179
372
  ? ` - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: "" # Set via: ci secrets add GOOGLE_PLAY_SERVICE_ACCOUNT_JSON -w release\n`
180
373
  : "";
181
- const googleServicesStep = setup.googleServices
374
+ const secretsPropertiesEnv = setup.secretsProperties
375
+ ? declare("LOCAL_PROPERTIES", discriminators(setup.workflowFlavors, ""), (wf) => wf
376
+ ? ` # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES -w ${wf}`
377
+ : ` # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES`)
378
+ : "";
379
+ /**
380
+ * The Secrets Gradle Plugin's properties file, written into every workflow
381
+ * that builds. Its evaluation listener runs for every variant, so a debug
382
+ * build fails to *configure* without it — the error names a module of the
383
+ * user's own project and nothing connects it to a missing credential.
384
+ */
385
+ const secretsPropertiesStep = setup.secretsProperties
182
386
  ? `
183
387
  - file@1.0.0:
388
+ title: Setup ${setup.secretsPropertiesPath}
184
389
  is_skippable: true
185
390
  inputs:
186
- target_path: app/google-services.json
187
- var_name: GOOGLE_SERVICES_JSON
391
+ target_path: ${setup.secretsPropertiesPath}
392
+ var_name: LOCAL_PROPERTIES
188
393
  `
189
394
  : "";
395
+ /**
396
+ * Per workflow, exactly as the keystore is: the flavor a workflow builds
397
+ * decides where the file is written. A workflow whose variant names no
398
+ * flavor is unchanged — `app/google-services.json`.
399
+ *
400
+ * The `var_name` is the same in every workflow on purpose. Scoping a value
401
+ * to a workflow is the *secret's* job, not the step's: `CIBUILD_SW__<WF>__X`
402
+ * is decoded by `SecretsManager` and resolved by `EnvResolver` for the
403
+ * workflow being run, so one name here reads a different value there.
404
+ */
405
+ const googleServicesStepFor = (wf) => {
406
+ if (!setup.googleServices)
407
+ return "";
408
+ const path = setup.googleServicesPaths[wf] ?? "app/google-services.json";
409
+ return `
410
+ - file@1.0.0:
411
+ is_skippable: true
412
+ inputs:
413
+ target_path: ${path}
414
+ var_name: GOOGLE_SERVICES_JSON
415
+ `;
416
+ };
190
417
  const googlePlayStep = setup.googlePlayDeploy
191
418
  ? `
192
419
  - file@1.0.0:
@@ -241,14 +468,14 @@ app:
241
468
  - MODULE: app
242
469
  - BUILD_TYPE: Debug
243
470
  - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true
244
- ${keystoreEnv}${keystorePropertiesEnv}${googleServicesEnv}${googlePlayEnv}
471
+ ${keystoreEnv}${keystorePropertiesEnv}${googleServicesEnv}${googlePlayEnv}${secretsPropertiesEnv}
245
472
  workflows:
246
473
  primary:
247
474
  envs:
248
475
  - VARIANT: ${pv.variant}
249
476
  ${buildTypeEnvOverride(pv)}
250
477
  steps:
251
- ${keystoreStepFor("primary")}${googleServicesStep}
478
+ ${keystoreStepFor("primary")}${googleServicesStepFor("primary")}${secretsPropertiesStep}
252
479
  - set-java-version@1.0.0:
253
480
  inputs:
254
481
  java_version: '${javaVersion}'
@@ -288,7 +515,7 @@ ${keystoreStepFor("primary")}${googleServicesStep}
288
515
  - VARIANT: ${prv.variant}
289
516
  ${buildTypeEnvOverride(prv)}
290
517
  steps:
291
- ${keystoreStepFor("pull-request")}${googleServicesStep}
518
+ ${keystoreStepFor("pull-request")}${googleServicesStepFor("pull-request")}${secretsPropertiesStep}
292
519
  - set-java-version@1.0.0:
293
520
  inputs:
294
521
  java_version: '${javaVersion}'
@@ -327,7 +554,7 @@ ${keystoreStepFor("pull-request")}${googleServicesStep}
327
554
  - VARIANT: ${rv.variant}
328
555
  ${buildTypeEnvOverride(rv)}
329
556
  steps:
330
- ${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStep}
557
+ ${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStepFor("release")}${secretsPropertiesStep}
331
558
  - set-java-version@1.0.0:
332
559
  inputs:
333
560
  java_version: '${javaVersion}'
@@ -894,7 +1121,13 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
894
1121
  }
895
1122
  // 4. Prompt to generate setup steps for detected file issues
896
1123
  const setupNeeds = detectSetupNeeds(scanResult.warnings);
897
- const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} };
1124
+ // Which flavor each workflow builds decides where its google-services.json
1125
+ // is written, and — with no path to give it away — whether the properties
1126
+ // file can be one value or has to be one per workflow. Both maps are
1127
+ // derived from the variants chosen just above.
1128
+ const googleServicesPaths = googleServicesTargets(variants, productFlavors);
1129
+ const workflowFlavors = variantFlavors(variants, productFlavors);
1130
+ const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {}, googleServicesPaths, workflowFlavors, secretsProperties: false, secretsPropertiesPath: setupNeeds.secretsPropertiesPath };
898
1131
  const secretsManager = new SecretsManager();
899
1132
  const keystoreWorkflowGroups = [
900
1133
  { label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
@@ -986,15 +1219,69 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
986
1219
  setupOptions.googleServices = !!create;
987
1220
  }
988
1221
  if (setupOptions.googleServices) {
1222
+ // One collection per distinct target path. Workflows building the same
1223
+ // flavor write to the same place and are collected together; workflows
1224
+ // building different flavors need different files, so each is asked
1225
+ // separately and pinned to its own source set — which is what lets a
1226
+ // flavored repository auto-select rather than guess.
1227
+ //
1228
+ // The secret name is the same every time, and the *value* is stored per
1229
+ // workflow. That is what `SecretsManager` has always done, what
1230
+ // `ci secrets upload` encodes as `CIBUILD_SW__<WF>__<VAR>`, and what
1231
+ // `EnvResolver` resolves for the workflow being run.
1232
+ const byTarget = collectionGroups(discriminators(setupOptions.googleServicesPaths, "app/google-services.json"), (_path, workflows) => workflows.join(", "));
1233
+ for (const group of byTarget) {
1234
+ const flavorDir = /\/src\/([^/]+)\//.exec(group.discriminator)?.[1] ?? null;
1235
+ await collectFileSecret({
1236
+ displayName: flavorDir
1237
+ ? `google-services.json for the ${flavorDir} flavor`
1238
+ : "google-services.json",
1239
+ secretName: "GOOGLE_SERVICES_JSON",
1240
+ fileMatch: (name) => name === "google-services.json",
1241
+ // A flavored target takes the copy from that flavor's source set. An
1242
+ // unflavored one takes anything that is *not* a flavor's copy, so
1243
+ // the module root wins rather than an arbitrary source set.
1244
+ pathMatch: flavorDir
1245
+ ? (rel) => rel.includes(`/src/${flavorDir}/`) || rel.startsWith(`src/${flavorDir}/`)
1246
+ : (rel) => !/(?:^|\/)src\/[^/]+\//.test(rel),
1247
+ readAs: "text",
1248
+ // One group per call, not all of them: each group narrows the search
1249
+ // to its own flavor's source set, which is what lets a flavored
1250
+ // repository auto-select rather than ask.
1251
+ workflowGroups: [group],
1252
+ }, cwd, secretsManager, ni);
1253
+ }
1254
+ }
1255
+ }
1256
+ if (setupNeeds.secretsProperties) {
1257
+ const propertiesFile = setupNeeds.secretsPropertiesPath;
1258
+ if (ni) {
1259
+ setupOptions.secretsProperties = true;
1260
+ }
1261
+ else {
1262
+ const { create } = await prompts({
1263
+ type: "confirm",
1264
+ name: "create",
1265
+ message: `Generate a setup step for '${propertiesFile}' (read by the Secrets Gradle Plugin)?`,
1266
+ initial: true,
1267
+ });
1268
+ setupOptions.secretsProperties = !!create;
1269
+ }
1270
+ if (setupOptions.secretsProperties) {
1271
+ // Every workflow, not just release: the plugin's evaluation listener
1272
+ // runs for every variant, so a debug build cannot configure without it.
1273
+ //
1274
+ // And one group per flavor, because the plugin turns this file into
1275
+ // BuildConfig and a flavor is an environment. Every group in one call,
1276
+ // unlike google-services.json above: there is no per-flavor path
1277
+ // convention for a properties file to narrow the search by, so the
1278
+ // groups differ only in which answer they take.
989
1279
  await collectFileSecret({
990
- displayName: "google-services.json",
991
- secretName: "GOOGLE_SERVICES_JSON",
992
- fileMatch: (name) => name === "google-services.json",
1280
+ displayName: propertiesFile,
1281
+ secretName: "LOCAL_PROPERTIES",
1282
+ fileMatch: (name) => name === propertiesFile.split("/").pop(),
993
1283
  readAs: "text",
994
- workflowGroups: [
995
- { label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
996
- { label: "release", workflows: ["release"] },
997
- ],
1284
+ workflowGroups: propertiesFileGroups(setupOptions.workflowFlavors),
998
1285
  }, cwd, secretsManager, ni);
999
1286
  }
1000
1287
  }
@@ -1079,8 +1366,16 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
1079
1366
  console.log(" • Setup Keystore (KEYSTORE_BASE64)");
1080
1367
  if (setupOptions.keystoreProperties)
1081
1368
  console.log(" • Setup keystore.properties (KEYSTORE_PROPERTIES) — release only");
1082
- if (setupOptions.googleServices)
1083
- console.log(" • Setup google-services.json (GOOGLE_SERVICES_JSON)");
1369
+ if (setupOptions.googleServices) {
1370
+ const seen = new Set();
1371
+ for (const wf of GENERATED_WORKFLOWS) {
1372
+ const path = setupOptions.googleServicesPaths[wf];
1373
+ if (!path || seen.has(path))
1374
+ continue;
1375
+ seen.add(path);
1376
+ console.log(` • Setup ${path} (GOOGLE_SERVICES_JSON)`);
1377
+ }
1378
+ }
1084
1379
  if (setupOptions.googlePlayDeploy)
1085
1380
  console.log(` • Google Play deploy (${setupOptions.googlePlayPackageName}) — release only`);
1086
1381
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `ci init` asks once per flavor for the properties file the Secrets Gradle
3
+ * Plugin reads, and stores the answer against that flavor's workflows.
4
+ *
5
+ * The generator now declares this credential once per workflow when the
6
+ * workflows build different flavors, because two flavors are two environments
7
+ * and one value cannot serve both. Collection had not moved: `collectFileSecret`
8
+ * was called with a single group labelled *"every workflow"*, so `ci init`
9
+ * asked once and stored the same bytes against `primary`, `pull-request` and
10
+ * `release`. The declaration would then say three values are needed and the
11
+ * store would hold one, three times.
12
+ *
13
+ * The machinery for this was already there. `collectFileSecret` takes
14
+ * `workflowGroups`, prompts per group, and stores per workflow — that is how
15
+ * the keystore has always collected a debug key and a release key separately.
16
+ * The properties file was the one credential passing it a single group.
17
+ *
18
+ * What matters more than the split is that it is the *same* split: `ci init`
19
+ * groups by exactly the discriminator `credentialSlots` declares by, so the
20
+ * question asked and the slot declared cannot drift apart. One group means one
21
+ * slot and one question; two groups mean one slot per workflow and a question
22
+ * per group. That invariant is asserted directly rather than left to two
23
+ * copies of the same rule staying in step.
24
+ */
25
+ export {};
26
+ //# sourceMappingURL=each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"each-flavor-is-asked-for-its-own-properties-file.test.d.ts","sourceRoot":"","sources":["../../../src/commands/each-flavor-is-asked-for-its-own-properties-file.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}