@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.
- 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 +219 -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 +291 -0
- package/dist/src/commands/build.d.ts +144 -0
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +317 -22
- 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 +3 -2
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +30 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +146 -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-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 +18 -3
- 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
|
@@ -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":"
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
|
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:
|
|
187
|
-
var_name:
|
|
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")}${
|
|
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")}${
|
|
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}${
|
|
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
|
-
|
|
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:
|
|
991
|
-
secretName: "
|
|
992
|
-
fileMatch: (name) => name === "
|
|
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
|
-
|
|
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"}
|