@invarn/cibuild 2.7.4 → 2.7.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/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +222 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +324 -0
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts +29 -0
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts.map +1 -0
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.js +160 -0
- package/dist/src/commands/android-scanner.d.ts +9 -1
- package/dist/src/commands/android-scanner.d.ts.map +1 -1
- package/dist/src/commands/android-scanner.js +103 -3
- package/dist/src/commands/build.d.ts +128 -1
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +286 -49
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
- package/dist/src/commands/file-secret-collector.d.ts +13 -0
- package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
- package/dist/src/commands/file-secret-collector.js +25 -7
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts +38 -0
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts.map +1 -0
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.js +300 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +2 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
- package/dist/src/yaml/converter.d.ts +48 -0
- package/dist/src/yaml/converter.d.ts.map +1 -1
- package/dist/src/yaml/converter.js +117 -0
- package/dist/src/yaml/env-resolver.d.ts +52 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +99 -1
- package/dist/src/yaml/secrets-manager.d.ts +13 -3
- package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
- package/dist/src/yaml/secrets-manager.js +25 -14
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts +22 -0
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.js +97 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +9 -0
- package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
- package/dist/src/yaml/steps/ios-deps.js +19 -4
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +73 -20
- package/package.json +1 -1
|
@@ -135,6 +135,158 @@ export function secretsPropertiesFileFrom(warnings) {
|
|
|
135
135
|
}
|
|
136
136
|
return "local.properties";
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The product flavor a variant builds, out of the flavors the scanner found —
|
|
140
|
+
* `ciDynamicDebug` is a build of `ciDynamic`, `prodRelease` of `prod`.
|
|
141
|
+
*
|
|
142
|
+
* An Android variant name is the flavor followed by the capitalised build
|
|
143
|
+
* type, so the character after the flavor has to be an upper-case one. That
|
|
144
|
+
* check is load-bearing: without it a flavor called `pro` would claim
|
|
145
|
+
* `prodRelease`. The longest match wins, so `ciDynamic` beats `ci` on a
|
|
146
|
+
* repository that declares both.
|
|
147
|
+
*/
|
|
148
|
+
export function flavorOfVariant(variant, productFlavors) {
|
|
149
|
+
const matches = productFlavors
|
|
150
|
+
.filter((flavor) => {
|
|
151
|
+
if (flavor.length === 0 || !variant.startsWith(flavor))
|
|
152
|
+
return false;
|
|
153
|
+
const head = variant.slice(flavor.length)[0];
|
|
154
|
+
return head !== undefined && head === head.toUpperCase() && head !== head.toLowerCase();
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => b.length - a.length);
|
|
157
|
+
return matches[0] ?? null;
|
|
158
|
+
}
|
|
159
|
+
/** Where a flavor's `google-services.json` belongs inside the app module. */
|
|
160
|
+
export function googleServicesPathFor(flavor, module = "app") {
|
|
161
|
+
return flavor
|
|
162
|
+
? `${module}/src/${flavor}/google-services.json`
|
|
163
|
+
: `${module}/google-services.json`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The per-workflow path and key maps for `SetupOptions`, from the variants the
|
|
167
|
+
* workflows build and the flavors the scanner found.
|
|
168
|
+
*
|
|
169
|
+
* `app/google-services.json` used to be hardcoded here. For a flavored build
|
|
170
|
+
* that is the plugin's *fallback* location, not the right one, and writing
|
|
171
|
+
* there is silently wrong rather than loudly wrong: the plugin finds a file,
|
|
172
|
+
* parses it, and fails on a package name — which reads as a Firebase
|
|
173
|
+
* misconfiguration rather than as the wrong file having been delivered. That
|
|
174
|
+
* is what `bld_8e60d90ab4e55f85d2f72459` spent 1292 Gradle tasks proving.
|
|
175
|
+
*/
|
|
176
|
+
export function googleServicesTargets(variants, productFlavors, module = "app") {
|
|
177
|
+
const paths = {};
|
|
178
|
+
for (const [workflow, config] of variantByWorkflow(variants)) {
|
|
179
|
+
const flavor = flavorOfVariant(config.variant, productFlavors);
|
|
180
|
+
paths[workflow] = googleServicesPathFor(flavor, module);
|
|
181
|
+
}
|
|
182
|
+
return paths;
|
|
183
|
+
}
|
|
184
|
+
/** The three workflows every generated pipeline has, in the order they are emitted. */
|
|
185
|
+
export const GENERATED_WORKFLOWS = ["primary", "pull-request", "release"];
|
|
186
|
+
/** The variant each workflow builds, keyed by the workflow's name in the YAML. */
|
|
187
|
+
function variantByWorkflow(variants) {
|
|
188
|
+
return [
|
|
189
|
+
["primary", variants.primary],
|
|
190
|
+
["pull-request", variants.pullRequest],
|
|
191
|
+
["release", variants.release],
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The `workflowFlavors` map for `SetupOptions`, from the variants the
|
|
196
|
+
* workflows build and the flavors the scanner found.
|
|
197
|
+
*/
|
|
198
|
+
export function variantFlavors(variants, productFlavors) {
|
|
199
|
+
const flavors = {};
|
|
200
|
+
for (const [workflow, config] of variantByWorkflow(variants)) {
|
|
201
|
+
flavors[workflow] = flavorOfVariant(config.variant, productFlavors);
|
|
202
|
+
}
|
|
203
|
+
return flavors;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The `envs` slots one credential needs: one when every workflow that reads it
|
|
207
|
+
* wants the same bytes, one per workflow when they do not.
|
|
208
|
+
*
|
|
209
|
+
* What "the same bytes" means is the credential's own business — a keystore
|
|
210
|
+
* follows the target path the pipeline declares for it, `google-services.json`
|
|
211
|
+
* the flavor's source set, `local.properties` the flavor itself — so the
|
|
212
|
+
* caller passes the discriminator it already computed, one entry per workflow
|
|
213
|
+
* that reads the credential, and this decides only whether they agree. A
|
|
214
|
+
* credential only one workflow reads can never disagree, which is why the
|
|
215
|
+
* genuinely release-only ones keep one slot however flavored the repository
|
|
216
|
+
* is.
|
|
217
|
+
*
|
|
218
|
+
* **The name never changes — the block it sits in does.** An agreed credential
|
|
219
|
+
* is declared once in `app.envs`; a disagreement is declared under the plain
|
|
220
|
+
* credential name in **each workflow's own `envs`**, which is where the YAML
|
|
221
|
+
* already says which workflow something belongs to. `EnvResolver` merges
|
|
222
|
+
* `app.envs` and then the running workflow's own, so `release`'s declaration is
|
|
223
|
+
* invisible to `primary` without anything being encoded.
|
|
224
|
+
*
|
|
225
|
+
* `CIBUILD_SW__<WORKFLOW>__<VAR>` is deliberately *not* used here. That prefix
|
|
226
|
+
* is the transport encoding `ci secrets upload` carries a workflow-scoped
|
|
227
|
+
* **value** under — Invarn's secret scopes have no workflow column, so the
|
|
228
|
+
* workflow travels in the key. A pipeline has structure instead, and spending
|
|
229
|
+
* the encoding there would be a second way to say what the block already says
|
|
230
|
+
* (Alex, 2026-09-10).
|
|
231
|
+
*
|
|
232
|
+
* Every workflow gets a slot, not only the odd one out: a workflow left without
|
|
233
|
+
* one would have neither a value nor the declaration `file@1.0.0` refuses over.
|
|
234
|
+
*
|
|
235
|
+
* The `file@` step's `var_name` is the same plain name either way.
|
|
236
|
+
*/
|
|
237
|
+
export function credentialSlots(varName, discriminatorByWorkflow) {
|
|
238
|
+
const distinct = new Set(Object.values(discriminatorByWorkflow));
|
|
239
|
+
if (distinct.size <= 1)
|
|
240
|
+
return [{ slot: varName, workflow: null }];
|
|
241
|
+
return Object.keys(discriminatorByWorkflow).map((workflow) => ({
|
|
242
|
+
slot: varName,
|
|
243
|
+
workflow,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* One discriminator per workflow, defaulted the way the step that writes the
|
|
248
|
+
* credential defaults its own input — so a repository whose scan resolved
|
|
249
|
+
* nothing does not read as three workflows that disagree.
|
|
250
|
+
*/
|
|
251
|
+
function discriminators(map, fallback) {
|
|
252
|
+
const out = {};
|
|
253
|
+
for (const workflow of GENERATED_WORKFLOWS)
|
|
254
|
+
out[workflow] = map[workflow] ?? fallback;
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The groups `ci init` collects a credential's value in: one per distinct value
|
|
259
|
+
* of whatever that credential follows, in workflow order.
|
|
260
|
+
*
|
|
261
|
+
* The same discriminator `credentialSlots` splits the *declaration* by, and
|
|
262
|
+
* deliberately the same function computing it, so what `ci init` asks for and
|
|
263
|
+
* what the pipeline declares cannot drift apart. One group means one slot and
|
|
264
|
+
* one question; two groups mean one slot per workflow and a question per group.
|
|
265
|
+
*/
|
|
266
|
+
export function collectionGroups(discriminatorByWorkflow, label) {
|
|
267
|
+
const byDiscriminator = new Map();
|
|
268
|
+
for (const [workflow, value] of Object.entries(discriminatorByWorkflow)) {
|
|
269
|
+
byDiscriminator.set(value, [...(byDiscriminator.get(value) ?? []), workflow]);
|
|
270
|
+
}
|
|
271
|
+
return [...byDiscriminator].map(([discriminator, workflows]) => ({
|
|
272
|
+
label: label(discriminator, workflows),
|
|
273
|
+
workflows,
|
|
274
|
+
discriminator,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The groups the Secrets Gradle Plugin's properties file is collected in: one
|
|
279
|
+
* per flavor.
|
|
280
|
+
*
|
|
281
|
+
* Unlike `google-services.json`, nothing about this credential's target says
|
|
282
|
+
* the bytes differ — every workflow writes the same file to the same path.
|
|
283
|
+
* What differs is the content: the plugin turns the file into `BuildConfig`,
|
|
284
|
+
* and a flavor is an environment. Asking once and storing one value gives
|
|
285
|
+
* `prod` the CI backend's base URL, on a green build.
|
|
286
|
+
*/
|
|
287
|
+
export function propertiesFileGroups(workflowFlavors) {
|
|
288
|
+
return collectionGroups(discriminators(workflowFlavors, ""), (flavor, workflows) => flavor ? `the ${flavor} flavor (${workflows.join(", ")})` : "every workflow");
|
|
289
|
+
}
|
|
138
290
|
export function detectSetupNeeds(warnings) {
|
|
139
291
|
const secretsPropertiesPath = secretsPropertiesFileFrom(warnings);
|
|
140
292
|
return {
|
|
@@ -174,7 +326,7 @@ trigger_map:
|
|
|
174
326
|
workflow: pull-request
|
|
175
327
|
`;
|
|
176
328
|
}
|
|
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) {
|
|
329
|
+
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) {
|
|
178
330
|
const keystoreStepFor = (wf) => {
|
|
179
331
|
if (!setup.keystore)
|
|
180
332
|
return "";
|
|
@@ -191,26 +343,58 @@ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: fa
|
|
|
191
343
|
};
|
|
192
344
|
// Add BUILD_TYPE override to workflow envs when it differs from the global default ("Debug")
|
|
193
345
|
const buildTypeEnvOverride = (v) => v.buildType !== "Debug" ? ` - BUILD_TYPE: ${v.buildType}\n` : "";
|
|
194
|
-
// Optional env var stubs (empty defaults — filled via ci secrets add)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
346
|
+
// Optional env var stubs (empty defaults — filled via ci secrets add).
|
|
347
|
+
//
|
|
348
|
+
// Declared and not merely used by a step: `unsetEnvKeys` — what `invarn
|
|
349
|
+
// setup` searches the developer's machine by — reads `envs`, never step
|
|
350
|
+
// inputs. An undeclared credential is never discovered and never offered for
|
|
351
|
+
// upload, however many steps name it. The same declaration is what
|
|
352
|
+
// `file@1.0.0` refuses over when nobody supplied the value.
|
|
353
|
+
//
|
|
354
|
+
// One slot per credential where every workflow that reads it wants the same
|
|
355
|
+
// bytes, and one per workflow where they do not — see `credentialSlots`. The
|
|
356
|
+
// name is the plain credential either way; a per-workflow one is declared in
|
|
357
|
+
// that workflow's own `envs` rather than under an encoded name.
|
|
358
|
+
const appEnvDeclarations = [];
|
|
359
|
+
const workflowEnvDeclarations = {};
|
|
360
|
+
const declare = (varName, discriminatorByWorkflow, comment) => {
|
|
361
|
+
for (const { slot, workflow } of credentialSlots(varName, discriminatorByWorkflow)) {
|
|
362
|
+
if (workflow === null) {
|
|
363
|
+
appEnvDeclarations.push(` - ${slot}: ""${comment(null)}\n`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
// Six spaces, not four: a workflow's `envs` sits one level deeper.
|
|
367
|
+
(workflowEnvDeclarations[workflow] ??= []).push(` - ${slot}: ""${comment(workflow)}\n`);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
/** Every credential declaration this workflow owns, after its own variables. */
|
|
371
|
+
const workflowEnvFor = (workflow) => (workflowEnvDeclarations[workflow] ?? []).join("");
|
|
372
|
+
// Declared in the order they appear in `app.envs`, which the byte-identity
|
|
373
|
+
// test on an unflavored pipeline pins.
|
|
374
|
+
if (setup.keystore) {
|
|
375
|
+
declare("KEYSTORE_BASE64", discriminators(setup.keystorePaths, "keystore.jks"), (wf) => wf
|
|
376
|
+
? ` # Set via: ci secrets add KEYSTORE_BASE64 -w ${wf}`
|
|
377
|
+
: ` # Set via: ci secrets add KEYSTORE_BASE64 -w primary`);
|
|
378
|
+
}
|
|
379
|
+
// Release-only, and legitimately so: nothing but `release` signs a release
|
|
380
|
+
// or talks to Google Play, so there is no second workflow to disagree with.
|
|
381
|
+
if (setup.keystoreProperties) {
|
|
382
|
+
appEnvDeclarations.push(` - KEYSTORE_PROPERTIES: "" # Release signing — set via: ci secrets add KEYSTORE_PROPERTIES -w release\n`);
|
|
383
|
+
}
|
|
384
|
+
if (setup.googleServices) {
|
|
385
|
+
declare("GOOGLE_SERVICES_JSON", discriminators(setup.googleServicesPaths, "app/google-services.json"), (wf) => wf
|
|
386
|
+
? ` # Set via: ci secrets add GOOGLE_SERVICES_JSON -w ${wf}`
|
|
387
|
+
: ` # Set via: ci secrets add GOOGLE_SERVICES_JSON`);
|
|
388
|
+
}
|
|
389
|
+
if (setup.googlePlayDeploy) {
|
|
390
|
+
appEnvDeclarations.push(` - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: "" # Set via: ci secrets add GOOGLE_PLAY_SERVICE_ACCOUNT_JSON -w release\n`);
|
|
391
|
+
}
|
|
392
|
+
if (setup.secretsProperties) {
|
|
393
|
+
declare("LOCAL_PROPERTIES", discriminators(setup.workflowFlavors, ""), (wf) => wf
|
|
394
|
+
? ` # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES -w ${wf}`
|
|
395
|
+
: ` # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES`);
|
|
396
|
+
}
|
|
397
|
+
const appEnv = appEnvDeclarations.join("");
|
|
214
398
|
/**
|
|
215
399
|
* The Secrets Gradle Plugin's properties file, written into every workflow
|
|
216
400
|
* that builds. Its evaluation listener runs for every variant, so a debug
|
|
@@ -227,15 +411,28 @@ export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: fa
|
|
|
227
411
|
var_name: LOCAL_PROPERTIES
|
|
228
412
|
`
|
|
229
413
|
: "";
|
|
230
|
-
|
|
231
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Per workflow, exactly as the keystore is: the flavor a workflow builds
|
|
416
|
+
* decides where the file is written. A workflow whose variant names no
|
|
417
|
+
* flavor is unchanged — `app/google-services.json`.
|
|
418
|
+
*
|
|
419
|
+
* The `var_name` is the same in every workflow on purpose. Scoping a value
|
|
420
|
+
* to a workflow is the *secret's* job, not the step's: `CIBUILD_SW__<WF>__X`
|
|
421
|
+
* is decoded by `SecretsManager` and resolved by `EnvResolver` for the
|
|
422
|
+
* workflow being run, so one name here reads a different value there.
|
|
423
|
+
*/
|
|
424
|
+
const googleServicesStepFor = (wf) => {
|
|
425
|
+
if (!setup.googleServices)
|
|
426
|
+
return "";
|
|
427
|
+
const path = setup.googleServicesPaths[wf] ?? "app/google-services.json";
|
|
428
|
+
return `
|
|
232
429
|
- file@1.0.0:
|
|
233
430
|
is_skippable: true
|
|
234
431
|
inputs:
|
|
235
|
-
target_path:
|
|
432
|
+
target_path: ${path}
|
|
236
433
|
var_name: GOOGLE_SERVICES_JSON
|
|
237
|
-
|
|
238
|
-
|
|
434
|
+
`;
|
|
435
|
+
};
|
|
239
436
|
const googlePlayStep = setup.googlePlayDeploy
|
|
240
437
|
? `
|
|
241
438
|
- file@1.0.0:
|
|
@@ -290,14 +487,14 @@ app:
|
|
|
290
487
|
- MODULE: app
|
|
291
488
|
- BUILD_TYPE: Debug
|
|
292
489
|
- GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true
|
|
293
|
-
${
|
|
490
|
+
${appEnv}
|
|
294
491
|
workflows:
|
|
295
492
|
primary:
|
|
296
493
|
envs:
|
|
297
494
|
- VARIANT: ${pv.variant}
|
|
298
|
-
${buildTypeEnvOverride(pv)}
|
|
495
|
+
${buildTypeEnvOverride(pv)}${workflowEnvFor("primary")}
|
|
299
496
|
steps:
|
|
300
|
-
${keystoreStepFor("primary")}${
|
|
497
|
+
${keystoreStepFor("primary")}${googleServicesStepFor("primary")}${secretsPropertiesStep}
|
|
301
498
|
- set-java-version@1.0.0:
|
|
302
499
|
inputs:
|
|
303
500
|
java_version: '${javaVersion}'
|
|
@@ -335,9 +532,9 @@ ${keystoreStepFor("primary")}${googleServicesStep}${secretsPropertiesStep}
|
|
|
335
532
|
pull-request:
|
|
336
533
|
envs:
|
|
337
534
|
- VARIANT: ${prv.variant}
|
|
338
|
-
${buildTypeEnvOverride(prv)}
|
|
535
|
+
${buildTypeEnvOverride(prv)}${workflowEnvFor("pull-request")}
|
|
339
536
|
steps:
|
|
340
|
-
${keystoreStepFor("pull-request")}${
|
|
537
|
+
${keystoreStepFor("pull-request")}${googleServicesStepFor("pull-request")}${secretsPropertiesStep}
|
|
341
538
|
- set-java-version@1.0.0:
|
|
342
539
|
inputs:
|
|
343
540
|
java_version: '${javaVersion}'
|
|
@@ -374,9 +571,9 @@ ${keystoreStepFor("pull-request")}${googleServicesStep}${secretsPropertiesStep}
|
|
|
374
571
|
release:
|
|
375
572
|
envs:
|
|
376
573
|
- VARIANT: ${rv.variant}
|
|
377
|
-
${buildTypeEnvOverride(rv)}
|
|
574
|
+
${buildTypeEnvOverride(rv)}${workflowEnvFor("release")}
|
|
378
575
|
steps:
|
|
379
|
-
${keystoreStepFor("release")}${keystorePropertiesStep}${
|
|
576
|
+
${keystoreStepFor("release")}${keystorePropertiesStep}${googleServicesStepFor("release")}${secretsPropertiesStep}
|
|
380
577
|
- set-java-version@1.0.0:
|
|
381
578
|
inputs:
|
|
382
579
|
java_version: '${javaVersion}'
|
|
@@ -943,7 +1140,13 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
943
1140
|
}
|
|
944
1141
|
// 4. Prompt to generate setup steps for detected file issues
|
|
945
1142
|
const setupNeeds = detectSetupNeeds(scanResult.warnings);
|
|
946
|
-
|
|
1143
|
+
// Which flavor each workflow builds decides where its google-services.json
|
|
1144
|
+
// is written, and — with no path to give it away — whether the properties
|
|
1145
|
+
// file can be one value or has to be one per workflow. Both maps are
|
|
1146
|
+
// derived from the variants chosen just above.
|
|
1147
|
+
const googleServicesPaths = googleServicesTargets(variants, productFlavors);
|
|
1148
|
+
const workflowFlavors = variantFlavors(variants, productFlavors);
|
|
1149
|
+
const setupOptions = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {}, googleServicesPaths, workflowFlavors, secretsProperties: false, secretsPropertiesPath: setupNeeds.secretsPropertiesPath };
|
|
947
1150
|
const secretsManager = new SecretsManager();
|
|
948
1151
|
const keystoreWorkflowGroups = [
|
|
949
1152
|
{ label: "debug builds (primary, pull-request)", workflows: ["primary", "pull-request"] },
|
|
@@ -1035,16 +1238,38 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
1035
1238
|
setupOptions.googleServices = !!create;
|
|
1036
1239
|
}
|
|
1037
1240
|
if (setupOptions.googleServices) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1241
|
+
// One collection per distinct target path. Workflows building the same
|
|
1242
|
+
// flavor write to the same place and are collected together; workflows
|
|
1243
|
+
// building different flavors need different files, so each is asked
|
|
1244
|
+
// separately and pinned to its own source set — which is what lets a
|
|
1245
|
+
// flavored repository auto-select rather than guess.
|
|
1246
|
+
//
|
|
1247
|
+
// The secret name is the same every time, and the *value* is stored per
|
|
1248
|
+
// workflow. That is what `SecretsManager` has always done, what
|
|
1249
|
+
// `ci secrets upload` encodes as `CIBUILD_SW__<WF>__<VAR>`, and what
|
|
1250
|
+
// `EnvResolver` resolves for the workflow being run.
|
|
1251
|
+
const byTarget = collectionGroups(discriminators(setupOptions.googleServicesPaths, "app/google-services.json"), (_path, workflows) => workflows.join(", "));
|
|
1252
|
+
for (const group of byTarget) {
|
|
1253
|
+
const flavorDir = /\/src\/([^/]+)\//.exec(group.discriminator)?.[1] ?? null;
|
|
1254
|
+
await collectFileSecret({
|
|
1255
|
+
displayName: flavorDir
|
|
1256
|
+
? `google-services.json for the ${flavorDir} flavor`
|
|
1257
|
+
: "google-services.json",
|
|
1258
|
+
secretName: "GOOGLE_SERVICES_JSON",
|
|
1259
|
+
fileMatch: (name) => name === "google-services.json",
|
|
1260
|
+
// A flavored target takes the copy from that flavor's source set. An
|
|
1261
|
+
// unflavored one takes anything that is *not* a flavor's copy, so
|
|
1262
|
+
// the module root wins rather than an arbitrary source set.
|
|
1263
|
+
pathMatch: flavorDir
|
|
1264
|
+
? (rel) => rel.includes(`/src/${flavorDir}/`) || rel.startsWith(`src/${flavorDir}/`)
|
|
1265
|
+
: (rel) => !/(?:^|\/)src\/[^/]+\//.test(rel),
|
|
1266
|
+
readAs: "text",
|
|
1267
|
+
// One group per call, not all of them: each group narrows the search
|
|
1268
|
+
// to its own flavor's source set, which is what lets a flavored
|
|
1269
|
+
// repository auto-select rather than ask.
|
|
1270
|
+
workflowGroups: [group],
|
|
1271
|
+
}, cwd, secretsManager, ni);
|
|
1272
|
+
}
|
|
1048
1273
|
}
|
|
1049
1274
|
}
|
|
1050
1275
|
if (setupNeeds.secretsProperties) {
|
|
@@ -1064,14 +1289,18 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
1064
1289
|
if (setupOptions.secretsProperties) {
|
|
1065
1290
|
// Every workflow, not just release: the plugin's evaluation listener
|
|
1066
1291
|
// runs for every variant, so a debug build cannot configure without it.
|
|
1292
|
+
//
|
|
1293
|
+
// And one group per flavor, because the plugin turns this file into
|
|
1294
|
+
// BuildConfig and a flavor is an environment. Every group in one call,
|
|
1295
|
+
// unlike google-services.json above: there is no per-flavor path
|
|
1296
|
+
// convention for a properties file to narrow the search by, so the
|
|
1297
|
+
// groups differ only in which answer they take.
|
|
1067
1298
|
await collectFileSecret({
|
|
1068
1299
|
displayName: propertiesFile,
|
|
1069
1300
|
secretName: "LOCAL_PROPERTIES",
|
|
1070
1301
|
fileMatch: (name) => name === propertiesFile.split("/").pop(),
|
|
1071
1302
|
readAs: "text",
|
|
1072
|
-
workflowGroups:
|
|
1073
|
-
{ label: "every workflow", workflows: ["primary", "pull-request", "release"] },
|
|
1074
|
-
],
|
|
1303
|
+
workflowGroups: propertiesFileGroups(setupOptions.workflowFlavors),
|
|
1075
1304
|
}, cwd, secretsManager, ni);
|
|
1076
1305
|
}
|
|
1077
1306
|
}
|
|
@@ -1156,8 +1385,16 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
1156
1385
|
console.log(" • Setup Keystore (KEYSTORE_BASE64)");
|
|
1157
1386
|
if (setupOptions.keystoreProperties)
|
|
1158
1387
|
console.log(" • Setup keystore.properties (KEYSTORE_PROPERTIES) — release only");
|
|
1159
|
-
if (setupOptions.googleServices)
|
|
1160
|
-
|
|
1388
|
+
if (setupOptions.googleServices) {
|
|
1389
|
+
const seen = new Set();
|
|
1390
|
+
for (const wf of GENERATED_WORKFLOWS) {
|
|
1391
|
+
const path = setupOptions.googleServicesPaths[wf];
|
|
1392
|
+
if (!path || seen.has(path))
|
|
1393
|
+
continue;
|
|
1394
|
+
seen.add(path);
|
|
1395
|
+
console.log(` • Setup ${path} (GOOGLE_SERVICES_JSON)`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1161
1398
|
if (setupOptions.googlePlayDeploy)
|
|
1162
1399
|
console.log(` • Google Play deploy (${setupOptions.googlePlayPackageName}) — release only`);
|
|
1163
1400
|
}
|
|
@@ -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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
26
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join, resolve } from 'node:path';
|
|
29
|
+
import prompts from 'prompts';
|
|
30
|
+
import { collectionGroups, credentialSlots, propertiesFileGroups, variantFlavors, } from './build.js';
|
|
31
|
+
import { collectFileSecret } from './file-secret-collector.js';
|
|
32
|
+
import { SecretsManager } from '../yaml/secrets-manager.js';
|
|
33
|
+
function variant(name) {
|
|
34
|
+
const buildType = name.charAt(0).toUpperCase() + name.slice(1);
|
|
35
|
+
return { variant: name, buildType, gradleTask: `assemble${buildType}` };
|
|
36
|
+
}
|
|
37
|
+
const FLAVORED = {
|
|
38
|
+
primary: variant('ciDynamicDebug'),
|
|
39
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
40
|
+
release: variant('prodRelease'),
|
|
41
|
+
};
|
|
42
|
+
const UNFLAVORED = {
|
|
43
|
+
primary: variant('debug'),
|
|
44
|
+
pullRequest: variant('debug'),
|
|
45
|
+
release: variant('release'),
|
|
46
|
+
};
|
|
47
|
+
const FLAVORS = ['ciDynamic', 'prod'];
|
|
48
|
+
const CI_DYNAMIC = 'BASE_API_URL=https://ci-dynamic.example.test/api\n';
|
|
49
|
+
const PROD = 'BASE_API_URL=https://prod.example.test/api\n';
|
|
50
|
+
describe('the groups a credential is collected in', () => {
|
|
51
|
+
test('are one when every workflow wants the same bytes', () => {
|
|
52
|
+
const groups = collectionGroups({ primary: 'ciDynamic', 'pull-request': 'ciDynamic', release: 'ciDynamic' }, () => 'every workflow');
|
|
53
|
+
expect(groups).toEqual([
|
|
54
|
+
{ label: 'every workflow', workflows: ['primary', 'pull-request', 'release'], discriminator: 'ciDynamic' },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
test('are one per distinct value, keeping the workflows that agree together', () => {
|
|
58
|
+
const groups = collectionGroups({ primary: 'ciDynamic', 'pull-request': 'ciDynamic', release: 'prod' }, (flavor) => flavor);
|
|
59
|
+
expect(groups).toEqual([
|
|
60
|
+
{ label: 'ciDynamic', workflows: ['primary', 'pull-request'], discriminator: 'ciDynamic' },
|
|
61
|
+
{ label: 'prod', workflows: ['release'], discriminator: 'prod' },
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
test('split exactly when the declaration splits', () => {
|
|
65
|
+
// Two copies of one rule is one copy too many. If these ever disagree,
|
|
66
|
+
// `ci init` asks a question the pipeline has no slot for, or declares a
|
|
67
|
+
// slot nothing on the machine was ever collected into.
|
|
68
|
+
const maps = [
|
|
69
|
+
{ primary: '', 'pull-request': '', release: '' },
|
|
70
|
+
{ primary: 'ciDynamic', 'pull-request': 'ciDynamic', release: 'prod' },
|
|
71
|
+
{ primary: 'a', 'pull-request': 'b', release: 'c' },
|
|
72
|
+
{ release: 'keystore.properties' },
|
|
73
|
+
{ primary: 'app/one.jks', 'pull-request': 'app/one.jks', release: 'app/one.jks' },
|
|
74
|
+
];
|
|
75
|
+
expect(maps.map((map) => ({
|
|
76
|
+
groups: collectionGroups(map, (d) => d).length === 1,
|
|
77
|
+
oneFlatSlot: credentialSlots('X', map).length === 1,
|
|
78
|
+
}))).toEqual([
|
|
79
|
+
{ groups: true, oneFlatSlot: true },
|
|
80
|
+
{ groups: false, oneFlatSlot: false },
|
|
81
|
+
{ groups: false, oneFlatSlot: false },
|
|
82
|
+
{ groups: true, oneFlatSlot: true },
|
|
83
|
+
{ groups: true, oneFlatSlot: true },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('the groups the properties file is collected in', () => {
|
|
88
|
+
test('are one per flavor, and say which flavor', () => {
|
|
89
|
+
const groups = propertiesFileGroups(variantFlavors(FLAVORED, FLAVORS));
|
|
90
|
+
expect(groups.map((g) => ({ label: g.label, workflows: g.workflows }))).toEqual([
|
|
91
|
+
{ label: 'the ciDynamic flavor (primary, pull-request)', workflows: ['primary', 'pull-request'] },
|
|
92
|
+
{ label: 'the prod flavor (release)', workflows: ['release'] },
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
test('are one, labelled as they were, on a repository with no flavors', () => {
|
|
96
|
+
// The label is what the prompt says out loud, and this is the wording
|
|
97
|
+
// every unflavored repository has always seen.
|
|
98
|
+
expect(propertiesFileGroups(variantFlavors(UNFLAVORED, []))).toEqual([
|
|
99
|
+
{ label: 'every workflow', workflows: ['primary', 'pull-request', 'release'], discriminator: '' },
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
test('are one when three workflows build three variants of one flavor', () => {
|
|
103
|
+
const oneFlavor = {
|
|
104
|
+
primary: variant('ciDynamicDebug'),
|
|
105
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
106
|
+
release: variant('ciDynamicRelease'),
|
|
107
|
+
};
|
|
108
|
+
expect(propertiesFileGroups(variantFlavors(oneFlavor, FLAVORS))).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('collecting a value for each group', () => {
|
|
112
|
+
let workdir;
|
|
113
|
+
let secretsManager;
|
|
114
|
+
// `prompts.inject` *concatenates*, so an empty array clears nothing and a
|
|
115
|
+
// sentinel one test leaves queued becomes the next test's answer. The queue
|
|
116
|
+
// itself is the only reset there is.
|
|
117
|
+
const clearInjected = () => {
|
|
118
|
+
prompts._injected = undefined;
|
|
119
|
+
};
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
clearInjected();
|
|
122
|
+
workdir = mkdtempSync(join(tmpdir(), 'cibuild-flavor-properties-'));
|
|
123
|
+
workdir = resolve(workdir);
|
|
124
|
+
secretsManager = new SecretsManager(join(workdir, '.cibuild-secrets.json'));
|
|
125
|
+
});
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
clearInjected();
|
|
128
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
129
|
+
});
|
|
130
|
+
function put(relPath, contents) {
|
|
131
|
+
const full = join(workdir, relPath);
|
|
132
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
133
|
+
writeFileSync(full, contents, 'utf-8');
|
|
134
|
+
return full;
|
|
135
|
+
}
|
|
136
|
+
async function collect(groups, ni = false) {
|
|
137
|
+
return collectFileSecret({
|
|
138
|
+
displayName: 'local.properties',
|
|
139
|
+
secretName: 'LOCAL_PROPERTIES',
|
|
140
|
+
fileMatch: (name) => name === 'local.properties',
|
|
141
|
+
readAs: 'text',
|
|
142
|
+
workflowGroups: groups,
|
|
143
|
+
}, workdir, secretsManager, ni);
|
|
144
|
+
}
|
|
145
|
+
test('stores a different value against the workflows of each flavor', async () => {
|
|
146
|
+
const ciDynamicFile = put('local.properties', CI_DYNAMIC);
|
|
147
|
+
const prodFile = put('envs/prod/local.properties', PROD);
|
|
148
|
+
prompts.inject([ciDynamicFile, prodFile]);
|
|
149
|
+
await collect(propertiesFileGroups(variantFlavors(FLAVORED, FLAVORS)));
|
|
150
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'primary')).toBe(CI_DYNAMIC);
|
|
151
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'pull-request')).toBe(CI_DYNAMIC);
|
|
152
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'release')).toBe(PROD);
|
|
153
|
+
});
|
|
154
|
+
test('stores them where `ci secrets upload` will encode them per workflow', async () => {
|
|
155
|
+
const ciDynamicFile = put('local.properties', CI_DYNAMIC);
|
|
156
|
+
const prodFile = put('envs/prod/local.properties', PROD);
|
|
157
|
+
prompts.inject([ciDynamicFile, prodFile]);
|
|
158
|
+
await collect(propertiesFileGroups(variantFlavors(FLAVORED, FLAVORS)));
|
|
159
|
+
// Not `global`. A global value would satisfy all three workflows and the
|
|
160
|
+
// upload would push one flat key, which is the bug this closes.
|
|
161
|
+
const stored = JSON.parse(readFileSync(join(workdir, '.cibuild-secrets.json'), 'utf-8'));
|
|
162
|
+
expect(stored.global).toEqual({});
|
|
163
|
+
expect(Object.keys(stored.workflows).sort()).toEqual(['primary', 'pull-request', 'release']);
|
|
164
|
+
});
|
|
165
|
+
test('asks nothing at all when one group meets one file', async () => {
|
|
166
|
+
// Today's behaviour on every unflavored repository, unchanged: a single
|
|
167
|
+
// group and a single candidate auto-apply. The injected answer is
|
|
168
|
+
// unreadable on purpose — if a prompt fired, nothing would be stored.
|
|
169
|
+
put('local.properties', CI_DYNAMIC);
|
|
170
|
+
prompts.inject(['/nonexistent/would-be-a-prompt']);
|
|
171
|
+
await collect(propertiesFileGroups(variantFlavors(UNFLAVORED, [])));
|
|
172
|
+
for (const workflow of ['primary', 'pull-request', 'release']) {
|
|
173
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', workflow)).toBe(CI_DYNAMIC);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
test('gives every group the same file when it cannot ask', async () => {
|
|
177
|
+
// `ci init --create` runs non-interactive. One file is all it can offer,
|
|
178
|
+
// and a wrong-flavored properties file that the user then corrects beats
|
|
179
|
+
// no properties file at all, which is a build that cannot configure.
|
|
180
|
+
put('local.properties', CI_DYNAMIC);
|
|
181
|
+
await collect(propertiesFileGroups(variantFlavors(FLAVORED, FLAVORS)), true);
|
|
182
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'primary')).toBe(CI_DYNAMIC);
|
|
183
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'release')).toBe(CI_DYNAMIC);
|
|
184
|
+
});
|
|
185
|
+
test('leaves a group alone once its value is already stored', async () => {
|
|
186
|
+
put('local.properties', CI_DYNAMIC);
|
|
187
|
+
put('envs/prod/local.properties', PROD);
|
|
188
|
+
secretsManager.storeSecret('LOCAL_PROPERTIES', PROD, 'release');
|
|
189
|
+
// One answer, for the one group still pending.
|
|
190
|
+
prompts.inject([join(workdir, 'local.properties')]);
|
|
191
|
+
await collect(propertiesFileGroups(variantFlavors(FLAVORED, FLAVORS)));
|
|
192
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'primary')).toBe(CI_DYNAMIC);
|
|
193
|
+
expect(secretsManager.getSecret('LOCAL_PROPERTIES', 'release')).toBe(PROD);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=each-flavor-is-asked-for-its-own-properties-file.test.js.map
|