@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
|
@@ -10,6 +10,19 @@ export interface FileSecretConfig {
|
|
|
10
10
|
secretName: string;
|
|
11
11
|
/** Returns true if a filename matches (e.g. name === "google-services.json") */
|
|
12
12
|
fileMatch: (name: string) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Narrows the candidates by their path relative to the project root, once
|
|
15
|
+
* `fileMatch` has found them by name.
|
|
16
|
+
*
|
|
17
|
+
* A credential Android keeps one copy of per product flavor has the same
|
|
18
|
+
* basename in every source set — four `google-services.json` under
|
|
19
|
+
* `app/`, `app/src/dev/`, `app/src/prod/` and `app/src/ciDynamic/` — so a
|
|
20
|
+
* name is not enough to say which one a workflow needs. Without this the
|
|
21
|
+
* single-candidate auto-apply below never fires on a flavored repository,
|
|
22
|
+
* and the answer comes from a prompt or from whichever the walk reached
|
|
23
|
+
* first.
|
|
24
|
+
*/
|
|
25
|
+
pathMatch?: (relativePath: string) => boolean;
|
|
13
26
|
/** How to read the file into a string value. Defaults to "text". */
|
|
14
27
|
readAs?: "text" | "base64";
|
|
15
28
|
/** Workflow groups to scope the secret to */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-secret-collector.d.ts","sourceRoot":"","sources":["../../../src/commands/file-secret-collector.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,6CAA6C;IAC7C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMrD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,EAAE,CAkBxF;AAiDD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,gBAAgB,EACxB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,cAAc,EAC9B,cAAc,UAAQ,GACrB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"file-secret-collector.d.ts","sourceRoot":"","sources":["../../../src/commands/file-secret-collector.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,6CAA6C;IAC7C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMrD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,EAAE,CAkBxF;AAiDD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,gBAAgB,EACxB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,cAAc,EAC9B,cAAc,UAAQ,GACrB,OAAO,CAAC,eAAe,CAAC,CA6I1B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, relative } from "node:path";
|
|
1
|
+
import { resolve, relative, sep } from "node:path";
|
|
2
2
|
import { readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import prompts from "prompts";
|
|
@@ -77,17 +77,35 @@ async function promptTargetPathInProject(displayName) {
|
|
|
77
77
|
*/
|
|
78
78
|
export async function collectFileSecret(config, cwd, secretsManager, nonInteractive = false) {
|
|
79
79
|
const paths = {};
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const readContent = (p) => config.readAs === "base64"
|
|
81
|
+
? readFileSync(p).toString("base64")
|
|
82
|
+
: readFileSync(p, "utf-8");
|
|
83
|
+
const relativeToCwd = (p) => relative(cwd, p).split(sep).join("/");
|
|
84
|
+
const candidates = findProjectFiles(cwd, config.fileMatch).filter((p) => config.pathMatch?.(relativeToCwd(p)) ?? true);
|
|
85
|
+
// A stored value answers "what are the bytes". It does not answer "where does
|
|
86
|
+
// each workflow write them", and `.cibuild-secrets.json` cannot: the store
|
|
87
|
+
// records values and never targets. So a caller that consumes the returned
|
|
88
|
+
// map has to be asked whichever state the value is in.
|
|
89
|
+
//
|
|
90
|
+
// `promptTargetPath` marks that caller — the keystore, the only one whose
|
|
91
|
+
// return value the generator assigns. Conflating the two questions lost the
|
|
92
|
+
// keystore split on every repository that had ever run `ci init`: a *global*
|
|
93
|
+
// entry satisfies `hasSecret` for every workflow, so nothing was pending, an
|
|
94
|
+
// empty map went back, and the generator's fallback handed all three
|
|
95
|
+
// workflows one path. A fresh clone asked and split correctly, which is why
|
|
96
|
+
// no fixture-built test saw it.
|
|
97
|
+
//
|
|
98
|
+
// Only when there is something on disk to choose between. With no candidate
|
|
99
|
+
// the question has no answer worth interrupting for, and the short-circuit is
|
|
100
|
+
// still the kinder branch.
|
|
101
|
+
const callerNeedsTargets = config.promptTargetPath === true && candidates.length > 0;
|
|
102
|
+
const pendingGroups = config.workflowGroups.filter((g) => callerNeedsTargets ||
|
|
103
|
+
!g.workflows.every((wf) => secretsManager.hasSecret(config.secretName, wf)));
|
|
82
104
|
if (pendingGroups.length === 0) {
|
|
83
105
|
console.log(` ✅ ${config.secretName} already in .cibuild-secrets.json — will be used at runtime.`);
|
|
84
106
|
return paths;
|
|
85
107
|
}
|
|
86
108
|
const pendingWorkflows = pendingGroups.flatMap((g) => g.workflows);
|
|
87
|
-
const readContent = (p) => config.readAs === "base64"
|
|
88
|
-
? readFileSync(p).toString("base64")
|
|
89
|
-
: readFileSync(p, "utf-8");
|
|
90
|
-
const candidates = findProjectFiles(cwd, config.fileMatch);
|
|
91
109
|
if (candidates.length === 1 && config.workflowGroups.length === 1) {
|
|
92
110
|
// Single file, single-group config — auto-apply without prompting
|
|
93
111
|
const content = readContent(candidates[0]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product flavors a convention plugin creates are still the project's flavors.
|
|
3
|
+
*
|
|
4
|
+
* `detectBuildVariants` looked for a literal `productFlavors { … }` block in a
|
|
5
|
+
* Gradle file. Modern Android does not write one: the flavors are an enum in a
|
|
6
|
+
* convention plugin's Kotlin source and are created programmatically —
|
|
7
|
+
* `AppFlavor.values().forEach { create(flavor.name) }` — so the block the
|
|
8
|
+
* scan looks for does not exist anywhere in the repository.
|
|
9
|
+
*
|
|
10
|
+
* `android-variant-resolution.test.ts` already records the *runtime* half of
|
|
11
|
+
* this: nowinandroid declares its flavors the same way, Gradle refused
|
|
12
|
+
* `:app:lintDebug` as ambiguous, and the step now reads the candidates out of
|
|
13
|
+
* Gradle's own error. That recovers a task name during a build. It cannot
|
|
14
|
+
* recover anything decided *before* the build:
|
|
15
|
+
*
|
|
16
|
+
* - `googleServicesTargets` writes every workflow's `google-services.json`
|
|
17
|
+
* to the module root instead of its flavor's source set, which is the
|
|
18
|
+
* silent wrong-file failure `bld_8e60d90ab4e55f85d2f72459` spent 1292
|
|
19
|
+
* Gradle tasks discovering;
|
|
20
|
+
* - `variantFlavors` reports no flavor for any workflow, so `credentialSlots`
|
|
21
|
+
* sees no disagreement and declares one flat slot where the workflows need
|
|
22
|
+
* one value each;
|
|
23
|
+
* - the variant prompt offers `debug, release` and nothing else.
|
|
24
|
+
*
|
|
25
|
+
* Gradle is not available to the scan — it is a static read, and running a
|
|
26
|
+
* configuration phase to list flavors would cost minutes. The evidence that
|
|
27
|
+
* *is* on disk is the one AGP itself relies on: a flavor has a source set.
|
|
28
|
+
* `app/src/ciDynamic/`, `app/src/prod/` — the same directories
|
|
29
|
+
* `findGoogleServices` in `invarn setup` has always read the flavor from,
|
|
30
|
+
* which is why setup resolves this repository correctly while the generator
|
|
31
|
+
* does not.
|
|
32
|
+
*
|
|
33
|
+
* It is a **fallback**, not a merge: a project whose Gradle files declare
|
|
34
|
+
* flavors literally is decided by those files exactly as before, so this can
|
|
35
|
+
* only change the answer for a project that currently detects none.
|
|
36
|
+
*/
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=flavors-a-convention-plugin-creates-are-still-found.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flavors-a-convention-plugin-creates-are-still-found.test.d.ts","sourceRoot":"","sources":["../../../src/commands/flavors-a-convention-plugin-creates-are-still-found.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product flavors a convention plugin creates are still the project's flavors.
|
|
3
|
+
*
|
|
4
|
+
* `detectBuildVariants` looked for a literal `productFlavors { … }` block in a
|
|
5
|
+
* Gradle file. Modern Android does not write one: the flavors are an enum in a
|
|
6
|
+
* convention plugin's Kotlin source and are created programmatically —
|
|
7
|
+
* `AppFlavor.values().forEach { create(flavor.name) }` — so the block the
|
|
8
|
+
* scan looks for does not exist anywhere in the repository.
|
|
9
|
+
*
|
|
10
|
+
* `android-variant-resolution.test.ts` already records the *runtime* half of
|
|
11
|
+
* this: nowinandroid declares its flavors the same way, Gradle refused
|
|
12
|
+
* `:app:lintDebug` as ambiguous, and the step now reads the candidates out of
|
|
13
|
+
* Gradle's own error. That recovers a task name during a build. It cannot
|
|
14
|
+
* recover anything decided *before* the build:
|
|
15
|
+
*
|
|
16
|
+
* - `googleServicesTargets` writes every workflow's `google-services.json`
|
|
17
|
+
* to the module root instead of its flavor's source set, which is the
|
|
18
|
+
* silent wrong-file failure `bld_8e60d90ab4e55f85d2f72459` spent 1292
|
|
19
|
+
* Gradle tasks discovering;
|
|
20
|
+
* - `variantFlavors` reports no flavor for any workflow, so `credentialSlots`
|
|
21
|
+
* sees no disagreement and declares one flat slot where the workflows need
|
|
22
|
+
* one value each;
|
|
23
|
+
* - the variant prompt offers `debug, release` and nothing else.
|
|
24
|
+
*
|
|
25
|
+
* Gradle is not available to the scan — it is a static read, and running a
|
|
26
|
+
* configuration phase to list flavors would cost minutes. The evidence that
|
|
27
|
+
* *is* on disk is the one AGP itself relies on: a flavor has a source set.
|
|
28
|
+
* `app/src/ciDynamic/`, `app/src/prod/` — the same directories
|
|
29
|
+
* `findGoogleServices` in `invarn setup` has always read the flavor from,
|
|
30
|
+
* which is why setup resolves this repository correctly while the generator
|
|
31
|
+
* does not.
|
|
32
|
+
*
|
|
33
|
+
* It is a **fallback**, not a merge: a project whose Gradle files declare
|
|
34
|
+
* flavors literally is decided by those files exactly as before, so this can
|
|
35
|
+
* only change the answer for a project that currently detects none.
|
|
36
|
+
*/
|
|
37
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
38
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
39
|
+
import { tmpdir } from 'node:os';
|
|
40
|
+
import { join, resolve } from 'node:path';
|
|
41
|
+
import { load } from 'js-yaml';
|
|
42
|
+
import { detectBuildVariants, scanAndroidProject } from './android-scanner.js';
|
|
43
|
+
import { generateAndroidPipeline, googleServicesTargets, variantFlavors, } from './build.js';
|
|
44
|
+
let root;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
root = resolve(mkdtempSync(join(tmpdir(), 'cibuild-convention-flavors-')));
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
rmSync(root, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
/** Writes `app/build.gradle.kts` with the given body and returns its path. */
|
|
52
|
+
function appModule(body) {
|
|
53
|
+
mkdirSync(join(root, 'app'), { recursive: true });
|
|
54
|
+
const path = join(root, 'app', 'build.gradle.kts');
|
|
55
|
+
writeFileSync(path, body, 'utf-8');
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
/** Creates `app/src/<name>/` holding each of `children`. */
|
|
59
|
+
function sourceSet(name, children = ['res']) {
|
|
60
|
+
const dir = join(root, 'app', 'src', name);
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
for (const child of children) {
|
|
63
|
+
if (child.includes('.'))
|
|
64
|
+
writeFileSync(join(dir, child), '{}', 'utf-8');
|
|
65
|
+
else
|
|
66
|
+
mkdirSync(join(dir, child), { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** An `android { buildTypes { … } }` block with no `productFlavors` anywhere. */
|
|
70
|
+
const CONVENTION_PLUGIN_MODULE = `
|
|
71
|
+
plugins {
|
|
72
|
+
id("acme.android.application")
|
|
73
|
+
id("acme.android.application.flavors")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
android {
|
|
77
|
+
namespace = "com.example.app"
|
|
78
|
+
buildTypes {
|
|
79
|
+
debug { isMinifyEnabled = false }
|
|
80
|
+
release { isMinifyEnabled = true }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
androidComponents {
|
|
85
|
+
onVariants { variant ->
|
|
86
|
+
if (variant.buildType == "release" &&
|
|
87
|
+
variant.productFlavors.any { it.second == "prod" }
|
|
88
|
+
) {
|
|
89
|
+
variant.signingConfig.setConfig(android.signingConfigs.getByName("release"))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
describe('flavors declared by a convention plugin', () => {
|
|
95
|
+
test('are read from the source sets the flavors have on disk', () => {
|
|
96
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
97
|
+
sourceSet('main', ['AndroidManifest.xml', 'java', 'res']);
|
|
98
|
+
sourceSet('ciDynamic', ['google-services.json', 'res']);
|
|
99
|
+
sourceSet('dev', ['google-services.json', 'res']);
|
|
100
|
+
sourceSet('prod', ['google-services.json', 'res']);
|
|
101
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['ciDynamic', 'dev', 'prod']);
|
|
102
|
+
});
|
|
103
|
+
test('become the variants the prompt offers', () => {
|
|
104
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
105
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
106
|
+
sourceSet('ciDynamic');
|
|
107
|
+
sourceSet('prod');
|
|
108
|
+
// Every flavor crossed with every build type, which is what the literal
|
|
109
|
+
// path has always produced.
|
|
110
|
+
expect(detectBuildVariants([gradle]).variants).toEqual([
|
|
111
|
+
'ciDynamicDebug',
|
|
112
|
+
'ciDynamicRelease',
|
|
113
|
+
'prodDebug',
|
|
114
|
+
'prodRelease',
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
test('are found in whichever module has them, not only `app`', () => {
|
|
118
|
+
const app = appModule(CONVENTION_PLUGIN_MODULE);
|
|
119
|
+
mkdirSync(join(root, 'core'), { recursive: true });
|
|
120
|
+
const core = join(root, 'core', 'build.gradle.kts');
|
|
121
|
+
writeFileSync(core, 'plugins { id("acme.android.library.flavors") }', 'utf-8');
|
|
122
|
+
mkdirSync(join(root, 'core', 'src', 'staging', 'res'), { recursive: true });
|
|
123
|
+
mkdirSync(join(root, 'core', 'src', 'main', 'java'), { recursive: true });
|
|
124
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
125
|
+
sourceSet('prod');
|
|
126
|
+
expect(detectBuildVariants([app, core]).productFlavors).toEqual(['prod', 'staging']);
|
|
127
|
+
});
|
|
128
|
+
test('are ordered the same way whatever order the filesystem lists them in', () => {
|
|
129
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
130
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
131
|
+
sourceSet('prod');
|
|
132
|
+
sourceSet('ciDynamic');
|
|
133
|
+
sourceSet('dev');
|
|
134
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['ciDynamic', 'dev', 'prod']);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('what a source set directory is not', () => {
|
|
138
|
+
test('the shared source set, the test source sets, and the build types', () => {
|
|
139
|
+
const gradle = appModule(`
|
|
140
|
+
android {
|
|
141
|
+
buildTypes {
|
|
142
|
+
debug { }
|
|
143
|
+
release { }
|
|
144
|
+
staging { }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
`);
|
|
148
|
+
sourceSet('main', ['AndroidManifest.xml', 'java']);
|
|
149
|
+
sourceSet('test', ['java']);
|
|
150
|
+
sourceSet('androidTest', ['java']);
|
|
151
|
+
sourceSet('testFixtures', ['java']);
|
|
152
|
+
sourceSet('debug', ['res']);
|
|
153
|
+
sourceSet('release', ['res']);
|
|
154
|
+
sourceSet('staging', ['res']);
|
|
155
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
test('a variant directory, which is a flavor and a build type already joined', () => {
|
|
158
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
159
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
160
|
+
sourceSet('ciDynamic');
|
|
161
|
+
sourceSet('ciDynamicDebug', ['res']);
|
|
162
|
+
sourceSet('prodRelease', ['res']);
|
|
163
|
+
// Taking `ciDynamicDebug` for a flavor would cross it with the build types
|
|
164
|
+
// again and offer `ciDynamicDebugDebug`.
|
|
165
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['ciDynamic']);
|
|
166
|
+
});
|
|
167
|
+
test('a test source set for one flavor', () => {
|
|
168
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
169
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
170
|
+
sourceSet('prod');
|
|
171
|
+
sourceSet('testProd', ['java']);
|
|
172
|
+
sourceSet('androidTestProd', ['java']);
|
|
173
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['prod']);
|
|
174
|
+
});
|
|
175
|
+
test('a flavor whose name merely starts with `test`', () => {
|
|
176
|
+
// `testing` is a legitimate flavor name; `testProd` is AGP's test source
|
|
177
|
+
// set for `prod`. The capital is the whole difference.
|
|
178
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
179
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
180
|
+
sourceSet('testing');
|
|
181
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['testing']);
|
|
182
|
+
});
|
|
183
|
+
test('a directory under `src/` that holds nothing a source set holds', () => {
|
|
184
|
+
const gradle = appModule(CONVENTION_PLUGIN_MODULE);
|
|
185
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
186
|
+
sourceSet('prod');
|
|
187
|
+
mkdirSync(join(root, 'app', 'src', 'notes'), { recursive: true });
|
|
188
|
+
writeFileSync(join(root, 'app', 'src', 'notes', 'README.md'), '# scratch', 'utf-8');
|
|
189
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['prod']);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('a project the literal scan already reads', () => {
|
|
193
|
+
test('is decided by its Gradle files, and the source sets change nothing', () => {
|
|
194
|
+
// The block declares two; the disk carries a third. A merge would invent a
|
|
195
|
+
// flavor the build does not have, so the literal answer wins outright.
|
|
196
|
+
const gradle = appModule(`
|
|
197
|
+
android {
|
|
198
|
+
flavorDimensions += "contentType"
|
|
199
|
+
productFlavors {
|
|
200
|
+
create("demo") { dimension = "contentType" }
|
|
201
|
+
create("prod") { dimension = "contentType" }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`);
|
|
205
|
+
sourceSet('main', ['AndroidManifest.xml']);
|
|
206
|
+
sourceSet('demo');
|
|
207
|
+
sourceSet('prod');
|
|
208
|
+
sourceSet('leftover', ['res']);
|
|
209
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual(['demo', 'prod']);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('a project with no flavors at all', () => {
|
|
213
|
+
test('is exactly what it was', () => {
|
|
214
|
+
const gradle = appModule(`
|
|
215
|
+
android {
|
|
216
|
+
buildTypes {
|
|
217
|
+
debug { }
|
|
218
|
+
release { }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`);
|
|
222
|
+
sourceSet('main', ['AndroidManifest.xml', 'java', 'res']);
|
|
223
|
+
sourceSet('debug', ['res']);
|
|
224
|
+
const detected = detectBuildVariants([gradle]);
|
|
225
|
+
expect(detected.productFlavors).toEqual([]);
|
|
226
|
+
expect(detected.variants).toEqual(['debug', 'release']);
|
|
227
|
+
});
|
|
228
|
+
test('is unchanged when the module has no `src/` directory at all', () => {
|
|
229
|
+
const gradle = appModule('android { }');
|
|
230
|
+
expect(detectBuildVariants([gradle]).productFlavors).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
const BASE_SETUP = {
|
|
234
|
+
keystore: false,
|
|
235
|
+
keystoreProperties: false,
|
|
236
|
+
googleServices: false,
|
|
237
|
+
googlePlayDeploy: false,
|
|
238
|
+
googlePlayPackageName: '',
|
|
239
|
+
artifactType: 'apk',
|
|
240
|
+
keystorePaths: {},
|
|
241
|
+
googleServicesPaths: {},
|
|
242
|
+
workflowFlavors: {},
|
|
243
|
+
secretsProperties: false,
|
|
244
|
+
secretsPropertiesPath: 'local.properties',
|
|
245
|
+
};
|
|
246
|
+
describe('what finding them buys the generator', () => {
|
|
247
|
+
/** The real repository's shape, as a project on disk. */
|
|
248
|
+
function conventionPluginRepository() {
|
|
249
|
+
appModule(CONVENTION_PLUGIN_MODULE);
|
|
250
|
+
writeFileSync(join(root, 'settings.gradle.kts'), 'include(":app")\n', 'utf-8');
|
|
251
|
+
sourceSet('main', ['AndroidManifest.xml', 'java', 'res']);
|
|
252
|
+
sourceSet('ciDynamic', ['google-services.json', 'res']);
|
|
253
|
+
sourceSet('dev', ['google-services.json', 'res']);
|
|
254
|
+
sourceSet('prod', ['google-services.json', 'res']);
|
|
255
|
+
}
|
|
256
|
+
const variant = (name) => ({
|
|
257
|
+
variant: name,
|
|
258
|
+
buildType: name.charAt(0).toUpperCase() + name.slice(1),
|
|
259
|
+
gradleTask: `assemble${name.charAt(0).toUpperCase()}${name.slice(1)}`,
|
|
260
|
+
});
|
|
261
|
+
const VARIANTS = {
|
|
262
|
+
primary: variant('ciDynamicDebug'),
|
|
263
|
+
pullRequest: variant('ciDynamicDebug'),
|
|
264
|
+
release: variant('prodRelease'),
|
|
265
|
+
};
|
|
266
|
+
test('each workflow writes google-services.json into its own source set', async () => {
|
|
267
|
+
conventionPluginRepository();
|
|
268
|
+
const { productFlavors } = (await scanAndroidProject(root)).buildVariants;
|
|
269
|
+
expect(googleServicesTargets(VARIANTS, productFlavors)).toEqual({
|
|
270
|
+
primary: 'app/src/ciDynamic/google-services.json',
|
|
271
|
+
'pull-request': 'app/src/ciDynamic/google-services.json',
|
|
272
|
+
release: 'app/src/prod/google-services.json',
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
test('the credential the two flavors cannot share is declared per workflow', async () => {
|
|
276
|
+
conventionPluginRepository();
|
|
277
|
+
const { productFlavors } = (await scanAndroidProject(root)).buildVariants;
|
|
278
|
+
const yaml = generateAndroidPipeline(17, {
|
|
279
|
+
...BASE_SETUP,
|
|
280
|
+
googleServices: true,
|
|
281
|
+
googleServicesPaths: googleServicesTargets(VARIANTS, productFlavors),
|
|
282
|
+
}, VARIANTS);
|
|
283
|
+
const parsed = load(yaml);
|
|
284
|
+
const declaringWorkflows = Object.keys(parsed.workflows ?? {}).filter((workflow) => 'GOOGLE_SERVICES_JSON' in Object.assign({}, ...(parsed.workflows?.[workflow]?.envs ?? [])));
|
|
285
|
+
// One declaration per workflow, in that workflow's own `envs`, under the
|
|
286
|
+
// plain name — the split the flavors bought, without the transport prefix.
|
|
287
|
+
expect(declaringWorkflows).toEqual(['primary', 'pull-request', 'release']);
|
|
288
|
+
expect(Object.assign({}, ...(parsed.app?.envs ?? []))).not.toHaveProperty('GOOGLE_SERVICES_JSON');
|
|
289
|
+
});
|
|
290
|
+
test('local.properties is split by the flavor each workflow builds', async () => {
|
|
291
|
+
conventionPluginRepository();
|
|
292
|
+
const { productFlavors } = (await scanAndroidProject(root)).buildVariants;
|
|
293
|
+
expect(variantFlavors(VARIANTS, productFlavors)).toEqual({
|
|
294
|
+
primary: 'ciDynamic',
|
|
295
|
+
'pull-request': 'ciDynamic',
|
|
296
|
+
release: 'prod',
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
//# sourceMappingURL=flavors-a-convention-plugin-creates-are-still-found.test.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `cache_paths` entry must not contain the file a `file@` step writes.
|
|
3
|
+
*
|
|
4
|
+
* Everything else about a credential's life inside a build is bounded, and was
|
|
5
|
+
* checked on the three boxes after the first build that ever wrote one: the
|
|
6
|
+
* runner pushes the checkout and the guest root onto `cleanupPaths` and
|
|
7
|
+
* removes them in a `finally`, the guest gets `rm -rf`, and `tart delete`
|
|
8
|
+
* reclaims the VM. Nothing was left anywhere.
|
|
9
|
+
*
|
|
10
|
+
* The cache store is the one place a file can survive all of that.
|
|
11
|
+
* `cache-push` archives whatever `cache_paths` names and uploads it to where
|
|
12
|
+
* the *next* build pulls it from, so a credential swept into that archive
|
|
13
|
+
* outlives the build it was delivered to — and outlives the sandbox that was
|
|
14
|
+
* supposed to contain it.
|
|
15
|
+
*
|
|
16
|
+
* No pipeline in play trips this. Every one of them caches
|
|
17
|
+
* `~/.gradle/caches`, `~/.gradle/wrapper` and `.gradle`, and none of those
|
|
18
|
+
* holds a credential. It is a guard on a mistake that is easy to make and
|
|
19
|
+
* invisible once made: adding `.` to `cache_paths` is a plausible way to make
|
|
20
|
+
* a build faster, and it would publish the signing key.
|
|
21
|
+
*
|
|
22
|
+
* Refused at generation time, like issue 03's missing credential, because that
|
|
23
|
+
* is the only place that can see a whole workflow at once — a cache step can
|
|
24
|
+
* be written before the `file@` step whose target it would swallow — and
|
|
25
|
+
* because the alternative is a build that goes green while leaking.
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=a-cache-path-must-not-swallow-a-credential.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a-cache-path-must-not-swallow-a-credential.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/a-cache-path-must-not-swallow-a-credential.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}
|