@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential's target path and the value chosen for it both have to follow
|
|
3
|
+
* the product flavor the workflow builds.
|
|
4
|
+
*
|
|
5
|
+
* An Android module has one source set per flavor, and each carries its own
|
|
6
|
+
* copies of whatever the build needs — the manifest, the resources, and
|
|
7
|
+
* `google-services.json`. The google-services plugin resolves the file by
|
|
8
|
+
* specificity (variant, then build type, then flavor, then the module root)
|
|
9
|
+
* and takes the first it finds. So a workflow building `ciDynamicDebug` wants
|
|
10
|
+
* `app/src/ciDynamic/google-services.json`, and only falls back to the module
|
|
11
|
+
* root when that is absent.
|
|
12
|
+
*
|
|
13
|
+
* The generator assumed one file at the module root. Writing there on a
|
|
14
|
+
* flavored repository is *silently* wrong rather than loudly wrong: the plugin
|
|
15
|
+
* finds a file, parses it, and fails on a package name. That is what
|
|
16
|
+
* `bld_8e60d90ab4e55f85d2f72459` (2026-09-09) spent 1292 Gradle tasks
|
|
17
|
+
* discovering:
|
|
18
|
+
*
|
|
19
|
+
* Execution failed for task ':app:processCiDynamicDebugGoogleServices'.
|
|
20
|
+
* > No matching client found for package name '….ciDynamic'
|
|
21
|
+
*
|
|
22
|
+
* The chain worked. The file was the wrong one of four, and the error names
|
|
23
|
+
* Firebase rather than the credential we delivered.
|
|
24
|
+
*
|
|
25
|
+
* Two flavors cannot share one secret, and the key is how a value varies. A
|
|
26
|
+
* dispatched build's secrets are a flat key→value map — Invarn's scopes are
|
|
27
|
+
* org, connection and pipeline, with no workflow *column* — but a flat map
|
|
28
|
+
* carries a workflow perfectly well when the workflow is part of the key,
|
|
29
|
+
* which is what `ci secrets upload` has encoded as
|
|
30
|
+
* `CIBUILD_SW__<WORKFLOW>__<VAR>` all along. So the differing keys are
|
|
31
|
+
* per-workflow ones, not per-flavor ones: the flavor is a property of the
|
|
32
|
+
* variant the workflow builds, so keying by workflow is one indirection less,
|
|
33
|
+
* and it is the convention already implemented end to end. A per-flavor
|
|
34
|
+
* suffix (`GOOGLE_SERVICES_JSON_CIDYNAMIC`) was briefly shipped here as a
|
|
35
|
+
* second answer to the same question and was reverted (Alex, 2026-09-09).
|
|
36
|
+
*/
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=a-credential-target-follows-the-flavor-source-set.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a-credential-target-follows-the-flavor-source-set.test.d.ts","sourceRoot":"","sources":["../../../src/commands/a-credential-target-follows-the-flavor-source-set.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential's target path and the value chosen for it both have to follow
|
|
3
|
+
* the product flavor the workflow builds.
|
|
4
|
+
*
|
|
5
|
+
* An Android module has one source set per flavor, and each carries its own
|
|
6
|
+
* copies of whatever the build needs — the manifest, the resources, and
|
|
7
|
+
* `google-services.json`. The google-services plugin resolves the file by
|
|
8
|
+
* specificity (variant, then build type, then flavor, then the module root)
|
|
9
|
+
* and takes the first it finds. So a workflow building `ciDynamicDebug` wants
|
|
10
|
+
* `app/src/ciDynamic/google-services.json`, and only falls back to the module
|
|
11
|
+
* root when that is absent.
|
|
12
|
+
*
|
|
13
|
+
* The generator assumed one file at the module root. Writing there on a
|
|
14
|
+
* flavored repository is *silently* wrong rather than loudly wrong: the plugin
|
|
15
|
+
* finds a file, parses it, and fails on a package name. That is what
|
|
16
|
+
* `bld_8e60d90ab4e55f85d2f72459` (2026-09-09) spent 1292 Gradle tasks
|
|
17
|
+
* discovering:
|
|
18
|
+
*
|
|
19
|
+
* Execution failed for task ':app:processCiDynamicDebugGoogleServices'.
|
|
20
|
+
* > No matching client found for package name '….ciDynamic'
|
|
21
|
+
*
|
|
22
|
+
* The chain worked. The file was the wrong one of four, and the error names
|
|
23
|
+
* Firebase rather than the credential we delivered.
|
|
24
|
+
*
|
|
25
|
+
* Two flavors cannot share one secret, and the key is how a value varies. A
|
|
26
|
+
* dispatched build's secrets are a flat key→value map — Invarn's scopes are
|
|
27
|
+
* org, connection and pipeline, with no workflow *column* — but a flat map
|
|
28
|
+
* carries a workflow perfectly well when the workflow is part of the key,
|
|
29
|
+
* which is what `ci secrets upload` has encoded as
|
|
30
|
+
* `CIBUILD_SW__<WORKFLOW>__<VAR>` all along. So the differing keys are
|
|
31
|
+
* per-workflow ones, not per-flavor ones: the flavor is a property of the
|
|
32
|
+
* variant the workflow builds, so keying by workflow is one indirection less,
|
|
33
|
+
* and it is the convention already implemented end to end. A per-flavor
|
|
34
|
+
* suffix (`GOOGLE_SERVICES_JSON_CIDYNAMIC`) was briefly shipped here as a
|
|
35
|
+
* second answer to the same question and was reverted (Alex, 2026-09-09).
|
|
36
|
+
*/
|
|
37
|
+
import { describe, test, expect } from '@jest/globals';
|
|
38
|
+
import { load } from 'js-yaml';
|
|
39
|
+
import { generateAndroidPipeline, flavorOfVariant, googleServicesPathFor, googleServicesTargets, } from './build.js';
|
|
40
|
+
const BASE = {
|
|
41
|
+
keystore: false,
|
|
42
|
+
keystoreProperties: false,
|
|
43
|
+
googleServices: false,
|
|
44
|
+
googlePlayDeploy: false,
|
|
45
|
+
googlePlayPackageName: '',
|
|
46
|
+
artifactType: 'apk',
|
|
47
|
+
keystorePaths: {},
|
|
48
|
+
googleServicesPaths: {},
|
|
49
|
+
workflowFlavors: {},
|
|
50
|
+
secretsProperties: false,
|
|
51
|
+
secretsPropertiesPath: 'local.properties',
|
|
52
|
+
};
|
|
53
|
+
function variant(name) {
|
|
54
|
+
const buildType = name.charAt(0).toUpperCase() + name.slice(1);
|
|
55
|
+
return { variant: name, buildType, gradleTask: `assemble${buildType}` };
|
|
56
|
+
}
|
|
57
|
+
/** The real repository's shape: two debug workflows on one flavor, release on another. */
|
|
58
|
+
const FLAVORED = {
|
|
59
|
+
primary: variant('ciDynamicDebug'),
|
|
60
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
61
|
+
release: variant('prodRelease'),
|
|
62
|
+
};
|
|
63
|
+
const UNFLAVORED = {
|
|
64
|
+
primary: variant('debug'),
|
|
65
|
+
pullRequest: variant('debug'),
|
|
66
|
+
release: variant('release'),
|
|
67
|
+
};
|
|
68
|
+
const FLAVORS = ['ciDynamic', 'dev', 'prod', 'gandalf'];
|
|
69
|
+
function parse(yaml) {
|
|
70
|
+
return load(yaml);
|
|
71
|
+
}
|
|
72
|
+
function envs(parsed) {
|
|
73
|
+
return Object.assign({}, ...(parsed.app?.envs ?? []));
|
|
74
|
+
}
|
|
75
|
+
/** The `file@` step of `workflow` that writes a google-services.json, if any. */
|
|
76
|
+
function googleServicesStep(parsed, workflow) {
|
|
77
|
+
return (parsed.workflows?.[workflow]?.steps ?? [])
|
|
78
|
+
.filter((s) => Object.keys(s)[0] === 'file@1.0.0')
|
|
79
|
+
.map((s) => s['file@1.0.0'].inputs)
|
|
80
|
+
.find((inputs) => String(inputs.target_path).endsWith('google-services.json'));
|
|
81
|
+
}
|
|
82
|
+
function generate(variants, flavors) {
|
|
83
|
+
return parse(generateAndroidPipeline(17, {
|
|
84
|
+
...BASE,
|
|
85
|
+
googleServices: true,
|
|
86
|
+
googleServicesPaths: googleServicesTargets(variants, flavors),
|
|
87
|
+
}, variants));
|
|
88
|
+
}
|
|
89
|
+
describe('which flavor a variant builds', () => {
|
|
90
|
+
test('is the flavor the variant name starts with', () => {
|
|
91
|
+
expect(flavorOfVariant('ciDynamicDebug', FLAVORS)).toBe('ciDynamic');
|
|
92
|
+
expect(flavorOfVariant('prodRelease', FLAVORS)).toBe('prod');
|
|
93
|
+
expect(flavorOfVariant('devDebug', FLAVORS)).toBe('dev');
|
|
94
|
+
});
|
|
95
|
+
test('is nothing when the variant is a bare build type', () => {
|
|
96
|
+
expect(flavorOfVariant('debug', FLAVORS)).toBeNull();
|
|
97
|
+
expect(flavorOfVariant('release', FLAVORS)).toBeNull();
|
|
98
|
+
// A repository with no product flavors can never match one.
|
|
99
|
+
expect(flavorOfVariant('ciDynamicDebug', [])).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
test('needs the capital that starts the build type, so a prefix cannot claim it', () => {
|
|
102
|
+
// Without this check `pro` claims `prodRelease` and the credential lands
|
|
103
|
+
// in the wrong source set — the same arbitrariness, one layer down.
|
|
104
|
+
expect(flavorOfVariant('prodRelease', ['pro'])).toBeNull();
|
|
105
|
+
expect(flavorOfVariant('prodRelease', ['pro', 'prod'])).toBe('prod');
|
|
106
|
+
});
|
|
107
|
+
test('gives the longest flavor the win when two both match', () => {
|
|
108
|
+
expect(flavorOfVariant('ciDynamicDebug', ['ci', 'ciDynamic'])).toBe('ciDynamic');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('the path a flavor implies', () => {
|
|
112
|
+
test('is the module root when there is no flavor', () => {
|
|
113
|
+
expect(googleServicesPathFor(null)).toBe('app/google-services.json');
|
|
114
|
+
});
|
|
115
|
+
test('is that flavor\'s source set when there is one', () => {
|
|
116
|
+
expect(googleServicesPathFor('ciDynamic')).toBe('app/src/ciDynamic/google-services.json');
|
|
117
|
+
expect(googleServicesPathFor('prod')).toBe('app/src/prod/google-services.json');
|
|
118
|
+
});
|
|
119
|
+
test('follows the module when the app is not in `app/`', () => {
|
|
120
|
+
expect(googleServicesPathFor('ciDynamic', 'androidApp')).toBe('androidApp/src/ciDynamic/google-services.json');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('the pipeline a flavored repository gets', () => {
|
|
124
|
+
test('writes each workflow into its own flavor source set', () => {
|
|
125
|
+
const parsed = generate(FLAVORED, FLAVORS);
|
|
126
|
+
// One object per workflow so a failure names the workflow that lost it.
|
|
127
|
+
expect(['primary', 'pull-request', 'release'].map((workflow) => ({
|
|
128
|
+
workflow,
|
|
129
|
+
target_path: googleServicesStep(parsed, workflow)?.target_path,
|
|
130
|
+
}))).toEqual([
|
|
131
|
+
{ workflow: 'primary', target_path: 'app/src/ciDynamic/google-services.json' },
|
|
132
|
+
{ workflow: 'pull-request', target_path: 'app/src/ciDynamic/google-services.json' },
|
|
133
|
+
{ workflow: 'release', target_path: 'app/src/prod/google-services.json' },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
test('names one secret in every workflow, whatever the flavor', () => {
|
|
137
|
+
// The value differs per workflow; the *name* does not. Scoping a value to
|
|
138
|
+
// a workflow is the secret's job — `ci secrets add GOOGLE_SERVICES_JSON
|
|
139
|
+
// <pipeline> -w primary` stores it per workflow, `ci secrets upload`
|
|
140
|
+
// encodes it as `CIBUILD_SW__PRIMARY__GOOGLE_SERVICES_JSON`, and
|
|
141
|
+
// `EnvResolver` resolves it for the workflow being run. A second naming
|
|
142
|
+
// convention in the step would be a second way to say the same thing.
|
|
143
|
+
const parsed = generate(FLAVORED, FLAVORS);
|
|
144
|
+
expect(['primary', 'pull-request', 'release'].map((workflow) => googleServicesStep(parsed, workflow)?.var_name)).toEqual(['GOOGLE_SERVICES_JSON', 'GOOGLE_SERVICES_JSON', 'GOOGLE_SERVICES_JSON']);
|
|
145
|
+
});
|
|
146
|
+
test('declares that one secret under no invented per-flavor name', () => {
|
|
147
|
+
// Declared, and declared empty: `unsetEnvKeys` — what `invarn setup`
|
|
148
|
+
// searches the developer's machine by — reads `envs`, never step inputs.
|
|
149
|
+
// A key no `envs` entry names is never discovered and never uploaded.
|
|
150
|
+
//
|
|
151
|
+
// *How many* slots it takes is a later ruling and lives in
|
|
152
|
+
// `a-credential-two-workflows-cannot-share-is-declared-per-workflow`: two
|
|
153
|
+
// flavors need two values, and `invarn setup` fills one slot with one
|
|
154
|
+
// value, so a flavored repository declares one slot per workflow under the
|
|
155
|
+
// `CIBUILD_SW__` name the upload already encodes. What this test still
|
|
156
|
+
// holds is the naming: whatever the flavor, the credential is
|
|
157
|
+
// `GOOGLE_SERVICES_JSON` and never `GOOGLE_SERVICES_JSON_CIDYNAMIC`.
|
|
158
|
+
const yaml = generateAndroidPipeline(17, {
|
|
159
|
+
...BASE,
|
|
160
|
+
googleServices: true,
|
|
161
|
+
googleServicesPaths: googleServicesTargets(FLAVORED, FLAVORS),
|
|
162
|
+
}, FLAVORED);
|
|
163
|
+
// One declaration per workflow, in that workflow's own `envs`, under the
|
|
164
|
+
// plain credential name — the block says which workflow, so nothing is
|
|
165
|
+
// encoded into the key.
|
|
166
|
+
const parsed = parse(yaml);
|
|
167
|
+
const declaringWorkflows = Object.keys(parsed.workflows ?? {}).filter((workflow) => 'GOOGLE_SERVICES_JSON' in
|
|
168
|
+
Object.assign({}, ...(parsed.workflows?.[workflow]?.envs ?? [])));
|
|
169
|
+
expect(declaringWorkflows).toEqual(['primary', 'pull-request', 'release']);
|
|
170
|
+
expect(envs(parsed)).not.toHaveProperty('GOOGLE_SERVICES_JSON');
|
|
171
|
+
expect(yaml).not.toMatch(/GOOGLE_SERVICES_JSON_[A-Z]/);
|
|
172
|
+
expect(yaml).not.toContain('CIBUILD_SW__');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('a repository with no product flavors', () => {
|
|
176
|
+
test('is exactly what it was: one key, one path, in every workflow', () => {
|
|
177
|
+
const parsed = generate(UNFLAVORED, []);
|
|
178
|
+
expect(envs(parsed).GOOGLE_SERVICES_JSON).toBe('');
|
|
179
|
+
for (const workflow of ['primary', 'pull-request', 'release']) {
|
|
180
|
+
expect(googleServicesStep(parsed, workflow)).toMatchObject({
|
|
181
|
+
target_path: 'app/google-services.json',
|
|
182
|
+
var_name: 'GOOGLE_SERVICES_JSON',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
test('is unchanged even when the pipeline is generated with no options at all', () => {
|
|
187
|
+
// The defaulted `SetupOptions` has empty maps; the step must still fall
|
|
188
|
+
// back to the module root rather than emitting `undefined`.
|
|
189
|
+
const parsed = parse(generateAndroidPipeline(17, { ...BASE, googleServices: true }, UNFLAVORED));
|
|
190
|
+
expect(googleServicesStep(parsed, 'primary')).toMatchObject({
|
|
191
|
+
target_path: 'app/google-services.json',
|
|
192
|
+
var_name: 'GOOGLE_SERVICES_JSON',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
test('emits nothing at all when the scan found no google-services.json', () => {
|
|
196
|
+
const parsed = parse(generateAndroidPipeline(17, BASE, UNFLAVORED));
|
|
197
|
+
expect(envs(parsed)).not.toHaveProperty('GOOGLE_SERVICES_JSON');
|
|
198
|
+
for (const workflow of ['primary', 'pull-request', 'release']) {
|
|
199
|
+
expect(googleServicesStep(parsed, workflow)).toBeUndefined();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('a repository whose workflows all build one flavor', () => {
|
|
204
|
+
test('gets a single flavored key, written to that flavor everywhere', () => {
|
|
205
|
+
const allCiDynamic = {
|
|
206
|
+
primary: variant('ciDynamicDebug'),
|
|
207
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
208
|
+
release: variant('ciDynamicRelease'),
|
|
209
|
+
};
|
|
210
|
+
const parsed = generate(allCiDynamic, FLAVORS);
|
|
211
|
+
expect(Object.keys(envs(parsed)).filter((k) => k.startsWith('GOOGLE_SERVICES_JSON'))).toEqual([
|
|
212
|
+
'GOOGLE_SERVICES_JSON',
|
|
213
|
+
]);
|
|
214
|
+
for (const workflow of ['primary', 'pull-request', 'release']) {
|
|
215
|
+
expect(googleServicesStep(parsed, workflow)).toMatchObject({
|
|
216
|
+
target_path: 'app/src/ciDynamic/google-services.json',
|
|
217
|
+
var_name: 'GOOGLE_SERVICES_JSON',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
//# sourceMappingURL=a-credential-target-follows-the-flavor-source-set.test.js.map
|
package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential two workflows cannot share is declared once per workflow, not
|
|
3
|
+
* once for the pipeline.
|
|
4
|
+
*
|
|
5
|
+
* The generator declares each credential it finds as one empty `app.envs`
|
|
6
|
+
* slot — `- LOCAL_PROPERTIES: ""` — and emits the same unsuffixed `var_name`
|
|
7
|
+
* in every workflow's `file@` step. On a repository with one flavor that is
|
|
8
|
+
* exactly right. On a flavored one it is one value where the build needs two,
|
|
9
|
+
* and the pipeline says so nowhere: `invarn setup` finds one slot, uploads one
|
|
10
|
+
* value, and all three workflows read it.
|
|
11
|
+
*
|
|
12
|
+
* `LOCAL_PROPERTIES` is the case that matters most and the one with no target
|
|
13
|
+
* path to give it away. Every workflow writes the same `local.properties` to
|
|
14
|
+
* the same place; what belongs *in* it is the flavor's environment — a
|
|
15
|
+
* different base API URL plus twenty other keys the Secrets Gradle Plugin
|
|
16
|
+
* turns into `BuildConfig`. `ciDynamic` and `prod` are different environments,
|
|
17
|
+
* so `prodRelease` built against `ciDynamic`'s properties is a release
|
|
18
|
+
* pointing at the CI backend. Two credentials give themselves away by path
|
|
19
|
+
* instead, and both were measured: `google-services.json` per flavor source
|
|
20
|
+
* set (`bld_8e60d90ab4e55f85d2f72459`) and the keystore per declared target
|
|
21
|
+
* (`bld_f3cc37d81…`).
|
|
22
|
+
*
|
|
23
|
+
* The name is the whole mechanism. Invarn's secret scopes are org, connection
|
|
24
|
+
* and pipeline, with no workflow column for a value to vary along — but a flat
|
|
25
|
+
* key→value map carries a workflow perfectly well when the workflow is part of
|
|
26
|
+
* the key, which is what `ci secrets upload` has always encoded and
|
|
27
|
+
* `SecretsManager` has always decoded. So the slot names the workflow and the
|
|
28
|
+
* step does not: `var_name` stays unsuffixed in every workflow, the decoding
|
|
29
|
+
* does the mapping, and a second naming convention in the step would be a
|
|
30
|
+
* second way to say the same thing (`GOOGLE_SERVICES_JSON_CIDYNAMIC` was that
|
|
31
|
+
* second way, and it was reverted).
|
|
32
|
+
*
|
|
33
|
+
* What decides is whether the workflows disagree about the thing the
|
|
34
|
+
* credential follows, and each credential follows something different: a
|
|
35
|
+
* keystore its declared target path, `google-services.json` the flavor's
|
|
36
|
+
* source set, `local.properties` the flavor itself. A credential only one
|
|
37
|
+
* workflow reads can never disagree, which is why the two genuinely
|
|
38
|
+
* release-only credentials keep one slot however flavored the repository is.
|
|
39
|
+
*/
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts","sourceRoot":"","sources":["../../../src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG"}
|
package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential two workflows cannot share is declared once per workflow, not
|
|
3
|
+
* once for the pipeline.
|
|
4
|
+
*
|
|
5
|
+
* The generator declares each credential it finds as one empty `app.envs`
|
|
6
|
+
* slot — `- LOCAL_PROPERTIES: ""` — and emits the same unsuffixed `var_name`
|
|
7
|
+
* in every workflow's `file@` step. On a repository with one flavor that is
|
|
8
|
+
* exactly right. On a flavored one it is one value where the build needs two,
|
|
9
|
+
* and the pipeline says so nowhere: `invarn setup` finds one slot, uploads one
|
|
10
|
+
* value, and all three workflows read it.
|
|
11
|
+
*
|
|
12
|
+
* `LOCAL_PROPERTIES` is the case that matters most and the one with no target
|
|
13
|
+
* path to give it away. Every workflow writes the same `local.properties` to
|
|
14
|
+
* the same place; what belongs *in* it is the flavor's environment — a
|
|
15
|
+
* different base API URL plus twenty other keys the Secrets Gradle Plugin
|
|
16
|
+
* turns into `BuildConfig`. `ciDynamic` and `prod` are different environments,
|
|
17
|
+
* so `prodRelease` built against `ciDynamic`'s properties is a release
|
|
18
|
+
* pointing at the CI backend. Two credentials give themselves away by path
|
|
19
|
+
* instead, and both were measured: `google-services.json` per flavor source
|
|
20
|
+
* set (`bld_8e60d90ab4e55f85d2f72459`) and the keystore per declared target
|
|
21
|
+
* (`bld_f3cc37d81…`).
|
|
22
|
+
*
|
|
23
|
+
* The name is the whole mechanism. Invarn's secret scopes are org, connection
|
|
24
|
+
* and pipeline, with no workflow column for a value to vary along — but a flat
|
|
25
|
+
* key→value map carries a workflow perfectly well when the workflow is part of
|
|
26
|
+
* the key, which is what `ci secrets upload` has always encoded and
|
|
27
|
+
* `SecretsManager` has always decoded. So the slot names the workflow and the
|
|
28
|
+
* step does not: `var_name` stays unsuffixed in every workflow, the decoding
|
|
29
|
+
* does the mapping, and a second naming convention in the step would be a
|
|
30
|
+
* second way to say the same thing (`GOOGLE_SERVICES_JSON_CIDYNAMIC` was that
|
|
31
|
+
* second way, and it was reverted).
|
|
32
|
+
*
|
|
33
|
+
* What decides is whether the workflows disagree about the thing the
|
|
34
|
+
* credential follows, and each credential follows something different: a
|
|
35
|
+
* keystore its declared target path, `google-services.json` the flavor's
|
|
36
|
+
* source set, `local.properties` the flavor itself. A credential only one
|
|
37
|
+
* workflow reads can never disagree, which is why the two genuinely
|
|
38
|
+
* release-only credentials keep one slot however flavored the repository is.
|
|
39
|
+
*/
|
|
40
|
+
import { describe, test, expect } from '@jest/globals';
|
|
41
|
+
import { load } from 'js-yaml';
|
|
42
|
+
import { generateAndroidPipeline, googleServicesTargets, variantFlavors, credentialSlots, GENERATED_WORKFLOWS, } from './build.js';
|
|
43
|
+
function variant(name) {
|
|
44
|
+
const buildType = name.charAt(0).toUpperCase() + name.slice(1);
|
|
45
|
+
return { variant: name, buildType, gradleTask: `assemble${buildType}` };
|
|
46
|
+
}
|
|
47
|
+
/** The real repository's shape: two debug workflows on one flavor, release on another. */
|
|
48
|
+
const FLAVORED = {
|
|
49
|
+
primary: variant('ciDynamicDebug'),
|
|
50
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
51
|
+
release: variant('prodRelease'),
|
|
52
|
+
};
|
|
53
|
+
const UNFLAVORED = {
|
|
54
|
+
primary: variant('debug'),
|
|
55
|
+
pullRequest: variant('debug'),
|
|
56
|
+
release: variant('release'),
|
|
57
|
+
};
|
|
58
|
+
const FLAVORS = ['ciDynamic', 'dev', 'prod'];
|
|
59
|
+
/** The keystore targets the real repository resolves: debug key, release key. */
|
|
60
|
+
const SPLIT_KEYSTORES = {
|
|
61
|
+
primary: 'config/debug.keystore',
|
|
62
|
+
'pull-request': 'config/debug.keystore',
|
|
63
|
+
release: 'app/release.jks',
|
|
64
|
+
};
|
|
65
|
+
const BASE = {
|
|
66
|
+
keystore: false,
|
|
67
|
+
keystoreProperties: false,
|
|
68
|
+
googleServices: false,
|
|
69
|
+
googlePlayDeploy: false,
|
|
70
|
+
googlePlayPackageName: '',
|
|
71
|
+
artifactType: 'apk',
|
|
72
|
+
keystorePaths: {},
|
|
73
|
+
googleServicesPaths: {},
|
|
74
|
+
workflowFlavors: {},
|
|
75
|
+
secretsProperties: false,
|
|
76
|
+
secretsPropertiesPath: 'local.properties',
|
|
77
|
+
};
|
|
78
|
+
/** Every credential the Android generator knows how to declare, all switched on. */
|
|
79
|
+
const ALL_CREDENTIALS = {
|
|
80
|
+
keystore: true,
|
|
81
|
+
keystoreProperties: true,
|
|
82
|
+
googleServices: true,
|
|
83
|
+
googlePlayDeploy: true,
|
|
84
|
+
googlePlayPackageName: 'com.example.app',
|
|
85
|
+
artifactType: 'aab',
|
|
86
|
+
secretsProperties: true,
|
|
87
|
+
};
|
|
88
|
+
function parse(yaml) {
|
|
89
|
+
return load(yaml);
|
|
90
|
+
}
|
|
91
|
+
function envs(parsed) {
|
|
92
|
+
return Object.assign({}, ...(parsed.app?.envs ?? []));
|
|
93
|
+
}
|
|
94
|
+
/** What one workflow's own `envs` block declares. */
|
|
95
|
+
function workflowEnvs(parsed, workflow) {
|
|
96
|
+
return Object.assign({}, ...(parsed.workflows?.[workflow]?.envs ?? []));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The workflows whose own `envs` declare `varName`, in pipeline order.
|
|
100
|
+
*
|
|
101
|
+
* The name is the plain credential now, so *where* the declaration sits is the
|
|
102
|
+
* whole answer to which workflow it belongs to.
|
|
103
|
+
*/
|
|
104
|
+
function declaringWorkflows(parsed, varName) {
|
|
105
|
+
return Object.keys(parsed.workflows ?? {}).filter((workflow) => varName in workflowEnvs(parsed, workflow));
|
|
106
|
+
}
|
|
107
|
+
/** The `var_name` of every `file@` step in `workflow`, in order. */
|
|
108
|
+
function fileVarNames(parsed, workflow) {
|
|
109
|
+
return (parsed.workflows?.[workflow]?.steps ?? [])
|
|
110
|
+
.filter((s) => Object.keys(s)[0] === 'file@1.0.0')
|
|
111
|
+
.map((s) => s['file@1.0.0'].inputs.var_name);
|
|
112
|
+
}
|
|
113
|
+
/** The declared slots that carry `varName`, however they are named. */
|
|
114
|
+
function slotsFor(parsed, varName) {
|
|
115
|
+
return Object.keys(envs(parsed)).filter((key) => key === varName || key.endsWith(`__${varName}`));
|
|
116
|
+
}
|
|
117
|
+
function flavoredPipeline(over = {}) {
|
|
118
|
+
return parse(generateAndroidPipeline(17, {
|
|
119
|
+
...BASE,
|
|
120
|
+
...ALL_CREDENTIALS,
|
|
121
|
+
keystorePaths: SPLIT_KEYSTORES,
|
|
122
|
+
googleServicesPaths: googleServicesTargets(FLAVORED, FLAVORS),
|
|
123
|
+
workflowFlavors: variantFlavors(FLAVORED, FLAVORS),
|
|
124
|
+
...over,
|
|
125
|
+
}, FLAVORED));
|
|
126
|
+
}
|
|
127
|
+
describe('what a credential follows', () => {
|
|
128
|
+
test('is the flavor each workflow builds, for the properties file', () => {
|
|
129
|
+
expect(variantFlavors(FLAVORED, FLAVORS)).toEqual({
|
|
130
|
+
primary: 'ciDynamic',
|
|
131
|
+
'pull-request': 'ciDynamic',
|
|
132
|
+
release: 'prod',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
test('is nothing at all on a repository with no product flavors', () => {
|
|
136
|
+
expect(variantFlavors(UNFLAVORED, [])).toEqual({
|
|
137
|
+
primary: null,
|
|
138
|
+
'pull-request': null,
|
|
139
|
+
release: null,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('the slots one credential gets', () => {
|
|
144
|
+
test('are one, unsuffixed, when every workflow wants the same bytes', () => {
|
|
145
|
+
expect(credentialSlots('LOCAL_PROPERTIES', {
|
|
146
|
+
primary: 'ciDynamic',
|
|
147
|
+
'pull-request': 'ciDynamic',
|
|
148
|
+
release: 'ciDynamic',
|
|
149
|
+
})).toEqual([{ slot: 'LOCAL_PROPERTIES', workflow: null }]);
|
|
150
|
+
});
|
|
151
|
+
test('are one per workflow, under the plain name, when they do not', () => {
|
|
152
|
+
// The name never changes; the block it is declared in does.
|
|
153
|
+
// `CIBUILD_SW__<WF>__<VAR>` is the encoding `ci secrets upload` carries a
|
|
154
|
+
// workflow-scoped *value* under, and a pipeline has structure instead.
|
|
155
|
+
expect(credentialSlots('LOCAL_PROPERTIES', {
|
|
156
|
+
primary: 'ciDynamic',
|
|
157
|
+
'pull-request': 'ciDynamic',
|
|
158
|
+
release: 'prod',
|
|
159
|
+
})).toEqual([
|
|
160
|
+
{ slot: 'LOCAL_PROPERTIES', workflow: 'primary' },
|
|
161
|
+
{ slot: 'LOCAL_PROPERTIES', workflow: 'pull-request' },
|
|
162
|
+
{ slot: 'LOCAL_PROPERTIES', workflow: 'release' },
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
test('include the workflows that agree, not only the odd one out', () => {
|
|
166
|
+
// `pull-request` wants the same bytes as `primary`. It still needs a slot
|
|
167
|
+
// of its own: the encoded name is per workflow, and a workflow with no
|
|
168
|
+
// slot has no value and no tripwire.
|
|
169
|
+
const workflows = credentialSlots('KEYSTORE_BASE64', SPLIT_KEYSTORES).map((s) => s.workflow);
|
|
170
|
+
expect(workflows).toEqual(['primary', 'pull-request', 'release']);
|
|
171
|
+
});
|
|
172
|
+
test('are one when only one workflow reads the credential', () => {
|
|
173
|
+
expect(credentialSlots('KEYSTORE_PROPERTIES', { release: 'keystore.properties' })).toEqual([
|
|
174
|
+
{ slot: 'KEYSTORE_PROPERTIES', workflow: null },
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('a flavored repository', () => {
|
|
179
|
+
test('declares the properties file in each workflow, and not globally', () => {
|
|
180
|
+
const parsed = flavoredPipeline();
|
|
181
|
+
expect(declaringWorkflows(parsed, 'LOCAL_PROPERTIES')).toEqual([
|
|
182
|
+
'primary',
|
|
183
|
+
'pull-request',
|
|
184
|
+
'release',
|
|
185
|
+
]);
|
|
186
|
+
// Globally would be one value for three workflows, which is the bug.
|
|
187
|
+
expect(envs(parsed)).not.toHaveProperty('LOCAL_PROPERTIES');
|
|
188
|
+
});
|
|
189
|
+
test('declares google-services.json and the keystore per workflow too', () => {
|
|
190
|
+
const parsed = flavoredPipeline();
|
|
191
|
+
for (const varName of ['GOOGLE_SERVICES_JSON', 'KEYSTORE_BASE64']) {
|
|
192
|
+
expect(declaringWorkflows(parsed, varName)).toEqual([
|
|
193
|
+
'primary',
|
|
194
|
+
'pull-request',
|
|
195
|
+
'release',
|
|
196
|
+
]);
|
|
197
|
+
expect(envs(parsed)).not.toHaveProperty(varName);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
test('keeps one slot for the credentials only `release` reads', () => {
|
|
201
|
+
// Genuinely release-only, and legitimately so: nothing else signs a
|
|
202
|
+
// release or talks to Google Play. A second slot for a workflow that never
|
|
203
|
+
// reads the value is a value the user is asked for and nothing consumes.
|
|
204
|
+
const parsed = flavoredPipeline();
|
|
205
|
+
expect(slotsFor(parsed, 'KEYSTORE_PROPERTIES')).toEqual(['KEYSTORE_PROPERTIES']);
|
|
206
|
+
expect(slotsFor(parsed, 'GOOGLE_PLAY_SERVICE_ACCOUNT_JSON')).toEqual([
|
|
207
|
+
'GOOGLE_PLAY_SERVICE_ACCOUNT_JSON',
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
test('leaves the step naming the credential, not the slot', () => {
|
|
211
|
+
// The `file@` step never learns the convention exists. `EnvResolver`
|
|
212
|
+
// resolves the plain name for the workflow being run, so one name in the
|
|
213
|
+
// step reads a different value in each workflow.
|
|
214
|
+
const parsed = flavoredPipeline();
|
|
215
|
+
for (const workflow of GENERATED_WORKFLOWS) {
|
|
216
|
+
const names = fileVarNames(parsed, workflow);
|
|
217
|
+
expect(names.length).toBeGreaterThan(0);
|
|
218
|
+
expect(names.filter((n) => n.startsWith('CIBUILD_'))).toEqual([]);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
test('declares every slot empty, which is what `invarn setup` searches by', () => {
|
|
222
|
+
// `unsetEnvKeys` collects `envs` keys whose declared value is the empty
|
|
223
|
+
// string — from `app.envs` and from each workflow's own. A slot with
|
|
224
|
+
// anything else in it is never discovered on the developer's machine and
|
|
225
|
+
// never offered for upload.
|
|
226
|
+
const parsed = flavoredPipeline();
|
|
227
|
+
const isCredential = (k) => /^(KEYSTORE|GOOGLE|LOCAL)_/.test(k);
|
|
228
|
+
const declared = [
|
|
229
|
+
...Object.entries(envs(parsed)),
|
|
230
|
+
...Object.keys(parsed.workflows ?? {}).flatMap((w) => Object.entries(workflowEnvs(parsed, w))),
|
|
231
|
+
].filter(([key]) => isCredential(key));
|
|
232
|
+
// Three per-workflow slots each for the properties file,
|
|
233
|
+
// google-services.json and the keystore; one flat slot each for the two
|
|
234
|
+
// release-only ones.
|
|
235
|
+
expect(declared.length).toBe(11);
|
|
236
|
+
for (const [, value] of declared)
|
|
237
|
+
expect(value).toBe('');
|
|
238
|
+
});
|
|
239
|
+
test('spends no encoding on what the block already says', () => {
|
|
240
|
+
// The workflow a declaration belongs to is the block it sits in. The
|
|
241
|
+
// `CIBUILD_SW__` prefix stays what it has always been: how `ci secrets
|
|
242
|
+
// upload` transports a workflow-scoped value into a flat key→value store
|
|
243
|
+
// that has no workflow column (Alex, 2026-09-10).
|
|
244
|
+
const yaml = generateAndroidPipeline(17, {
|
|
245
|
+
...BASE,
|
|
246
|
+
...ALL_CREDENTIALS,
|
|
247
|
+
keystorePaths: SPLIT_KEYSTORES,
|
|
248
|
+
googleServicesPaths: googleServicesTargets(FLAVORED, FLAVORS),
|
|
249
|
+
workflowFlavors: variantFlavors(FLAVORED, FLAVORS),
|
|
250
|
+
}, FLAVORED);
|
|
251
|
+
expect(yaml).not.toContain('CIBUILD_SW__');
|
|
252
|
+
expect(yaml).not.toContain('CIBUILD_S__');
|
|
253
|
+
});
|
|
254
|
+
test('puts a per-workflow declaration in a workflow the pipeline has', () => {
|
|
255
|
+
// Structural now, and that is the point: a declaration cannot name a
|
|
256
|
+
// workflow that does not exist, because it lives inside one.
|
|
257
|
+
const parsed = flavoredPipeline();
|
|
258
|
+
const present = Object.keys(parsed.workflows ?? {});
|
|
259
|
+
const perWorkflow = ['KEYSTORE_BASE64', 'GOOGLE_SERVICES_JSON', 'LOCAL_PROPERTIES'].flatMap((varName) => declaringWorkflows(parsed, varName));
|
|
260
|
+
expect(perWorkflow.length).toBe(9);
|
|
261
|
+
expect(perWorkflow.every((w) => present.includes(w))).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('a repository whose workflows all want the same bytes', () => {
|
|
265
|
+
test('is byte-identical to what the generator emitted before any of this', () => {
|
|
266
|
+
// The whole `app.envs` block, as it stood at cibuild d8263a1 with every
|
|
267
|
+
// credential switched on and no flavors anywhere. Item 02 must be inert
|
|
268
|
+
// here: this is what every already-onboarded project regenerates to.
|
|
269
|
+
const yaml = generateAndroidPipeline(17, { ...BASE, ...ALL_CREDENTIALS });
|
|
270
|
+
const block = yaml.slice(yaml.indexOf('app:\n'), yaml.indexOf('\nworkflows:'));
|
|
271
|
+
expect(block).toBe('app:\n' +
|
|
272
|
+
' envs:\n' +
|
|
273
|
+
' - PROJECT_LOCATION: .\n' +
|
|
274
|
+
' - MODULE: app\n' +
|
|
275
|
+
' - BUILD_TYPE: Debug\n' +
|
|
276
|
+
' - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true\n' +
|
|
277
|
+
' - KEYSTORE_BASE64: "" # Set via: ci secrets add KEYSTORE_BASE64 -w primary\n' +
|
|
278
|
+
' - KEYSTORE_PROPERTIES: "" # Release signing — set via: ci secrets add KEYSTORE_PROPERTIES -w release\n' +
|
|
279
|
+
' - GOOGLE_SERVICES_JSON: "" # Set via: ci secrets add GOOGLE_SERVICES_JSON\n' +
|
|
280
|
+
' - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: "" # Set via: ci secrets add GOOGLE_PLAY_SERVICE_ACCOUNT_JSON -w release\n' +
|
|
281
|
+
' - LOCAL_PROPERTIES: "" # Read by the Secrets Gradle Plugin — set via: ci secrets add LOCAL_PROPERTIES\n');
|
|
282
|
+
expect(yaml).not.toContain('CIBUILD_SW__');
|
|
283
|
+
});
|
|
284
|
+
test('is flat on one flavor as much as on none', () => {
|
|
285
|
+
// Three workflows, one flavor, three different build types. The build type
|
|
286
|
+
// is not what any of these credentials follows, so nothing splits.
|
|
287
|
+
const allCiDynamic = {
|
|
288
|
+
primary: variant('ciDynamicDebug'),
|
|
289
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
290
|
+
release: variant('ciDynamicRelease'),
|
|
291
|
+
};
|
|
292
|
+
const yaml = generateAndroidPipeline(17, {
|
|
293
|
+
...BASE,
|
|
294
|
+
...ALL_CREDENTIALS,
|
|
295
|
+
googleServicesPaths: googleServicesTargets(allCiDynamic, FLAVORS),
|
|
296
|
+
workflowFlavors: variantFlavors(allCiDynamic, FLAVORS),
|
|
297
|
+
}, allCiDynamic);
|
|
298
|
+
expect(yaml).not.toContain('CIBUILD_SW__');
|
|
299
|
+
expect(slotsFor(parse(yaml), 'LOCAL_PROPERTIES')).toEqual(['LOCAL_PROPERTIES']);
|
|
300
|
+
});
|
|
301
|
+
test('is flat when one keystore serves every workflow', () => {
|
|
302
|
+
const yaml = generateAndroidPipeline(17, {
|
|
303
|
+
...BASE,
|
|
304
|
+
keystore: true,
|
|
305
|
+
keystorePaths: { primary: 'app/one.jks', 'pull-request': 'app/one.jks', release: 'app/one.jks' },
|
|
306
|
+
});
|
|
307
|
+
expect(slotsFor(parse(yaml), 'KEYSTORE_BASE64')).toEqual(['KEYSTORE_BASE64']);
|
|
308
|
+
});
|
|
309
|
+
test('is flat when the scan resolved no keystore path at all', () => {
|
|
310
|
+
// Empty maps default the way the step defaults them, so a repository the
|
|
311
|
+
// scan could not resolve does not read as three disagreeing workflows.
|
|
312
|
+
const yaml = generateAndroidPipeline(17, { ...BASE, keystore: true });
|
|
313
|
+
expect(slotsFor(parse(yaml), 'KEYSTORE_BASE64')).toEqual(['KEYSTORE_BASE64']);
|
|
314
|
+
});
|
|
315
|
+
test('is flat when only some workflows resolved a path, and it is the default', () => {
|
|
316
|
+
const yaml = generateAndroidPipeline(17, {
|
|
317
|
+
...BASE,
|
|
318
|
+
keystore: true,
|
|
319
|
+
keystorePaths: { release: 'keystore.jks' },
|
|
320
|
+
});
|
|
321
|
+
expect(slotsFor(parse(yaml), 'KEYSTORE_BASE64')).toEqual(['KEYSTORE_BASE64']);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
//# sourceMappingURL=a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js.map
|
package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value already in `.cibuild-secrets.json` answers "what are the bytes".
|
|
3
|
+
* It does not answer "where does each workflow write them", and
|
|
4
|
+
* `collectFileSecret` used to treat the two as one question.
|
|
5
|
+
*
|
|
6
|
+
* `pendingGroups` is empty when every group already has the secret — and a
|
|
7
|
+
* **global** entry satisfies `hasSecret(name, workflow)` for every workflow, so
|
|
8
|
+
* one `ci init` in the repository's past is enough. The call then returned an
|
|
9
|
+
* empty paths map, the caller assigned that to `setupOptions.keystorePaths`,
|
|
10
|
+
* and the fallback filled every workflow from one source: the `storeFile=`
|
|
11
|
+
* line of whatever `keystore.properties` was collected, or the first path
|
|
12
|
+
* Gradle mentioned. One path for three workflows is one discriminator, so
|
|
13
|
+
* `credentialSlots` declared a single flat `KEYSTORE_BASE64` and the whole
|
|
14
|
+
* per-workflow split was lost — silently, and on exactly the credential the
|
|
15
|
+
* split exists for.
|
|
16
|
+
*
|
|
17
|
+
* Measured on the real repository, 2026-09-09: the store held
|
|
18
|
+
* `KEYSTORE_BASE64` globally, the keystore prompt never appeared, and all
|
|
19
|
+
* three workflows were generated writing to `customer-android.jks` — the
|
|
20
|
+
* release key's path, over the two debug workflows. A fresh clone of the same
|
|
21
|
+
* repository prompts and splits correctly, which is why this never showed up
|
|
22
|
+
* in a test that builds its own fixture from nothing.
|
|
23
|
+
*
|
|
24
|
+
* The store records values and never paths, so the path has to be asked for
|
|
25
|
+
* whichever the value's state is. Only a caller that consumes the returned map
|
|
26
|
+
* is affected — `promptTargetPath` marks it — so nothing else gains a prompt.
|
|
27
|
+
*/
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=an-already-stored-value-does-not-answer-the-target-question.test.d.ts.map
|