@invarn/cibuild 2.7.4 → 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 +124 -1
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +251 -33
- 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.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-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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value `SecretsManager` was just told to store is a value it can read back.
|
|
3
|
+
*
|
|
4
|
+
* It could not, on exactly the repositories that matter. `loadSecrets` sets
|
|
5
|
+
* `fileLoaded` when it finds a `.cibuild-secrets.json` or a
|
|
6
|
+
* `CIBUILD_S*`-shaped environment, and every reader begins
|
|
7
|
+
* `if (!this.fileLoaded) return process.env[...]` — a compatibility fallback
|
|
8
|
+
* to bare `CIBUILD_`-prefixed variables from before the store existed.
|
|
9
|
+
* `storeSecret` writes the file but never clears the flag, so on a repository
|
|
10
|
+
* that had no store — a first `ci init`, which is the only time `ci init`
|
|
11
|
+
* collects anything — every value collected in that run read back as absent
|
|
12
|
+
* from the instance that stored it.
|
|
13
|
+
*
|
|
14
|
+
* It is not a hypothetical. `ci init` extracts a workflow's keystore target
|
|
15
|
+
* from `storeFile=` in the `keystore.properties` it collected a moment
|
|
16
|
+
* earlier, which is how a project whose signing config names
|
|
17
|
+
* `app/customer.jks` gets its `file@` step pointed there instead of at
|
|
18
|
+
* `keystore.jks`. That read returned undefined on every fresh repository, so
|
|
19
|
+
* the resolution silently fell through to the Gradle-detected path — the half
|
|
20
|
+
* of issue 07 that shipped and never fired.
|
|
21
|
+
*
|
|
22
|
+
* The fix is additive rather than a flag flip: the stored data is consulted
|
|
23
|
+
* first, and the environment fallback stays exactly where it was for anything
|
|
24
|
+
* the store does not hold. Nothing that resolved before stops resolving.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
27
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { tmpdir } from 'node:os';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { SecretsManager } from './secrets-manager.js';
|
|
31
|
+
const KEYSTORE_PROPERTIES = 'storeFile=app/customer.jks\nstorePassword=x\n';
|
|
32
|
+
let dir;
|
|
33
|
+
let storePath;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
dir = mkdtempSync(join(tmpdir(), 'cibuild-store-readback-'));
|
|
36
|
+
storePath = join(dir, '.cibuild-secrets.json');
|
|
37
|
+
delete process.env.CIBUILD_KEYSTORE_PROPERTIES;
|
|
38
|
+
delete process.env.CIBUILD_FROM_THE_ENVIRONMENT;
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
rmSync(dir, { recursive: true, force: true });
|
|
42
|
+
delete process.env.CIBUILD_KEYSTORE_PROPERTIES;
|
|
43
|
+
delete process.env.CIBUILD_FROM_THE_ENVIRONMENT;
|
|
44
|
+
});
|
|
45
|
+
describe('a repository that had no store yet', () => {
|
|
46
|
+
test('reads back a workflow-scoped value it just stored', () => {
|
|
47
|
+
const sm = new SecretsManager(storePath);
|
|
48
|
+
sm.storeSecret('KEYSTORE_PROPERTIES', KEYSTORE_PROPERTIES, 'release');
|
|
49
|
+
expect(sm.getSecret('KEYSTORE_PROPERTIES', 'release')).toBe(KEYSTORE_PROPERTIES);
|
|
50
|
+
expect(sm.hasSecret('KEYSTORE_PROPERTIES', 'release')).toBe(true);
|
|
51
|
+
expect(sm.getSecretIdByName('KEYSTORE_PROPERTIES', 'release')).toBe('KEYSTORE_PROPERTIES');
|
|
52
|
+
});
|
|
53
|
+
test('is why `ci init` can point a keystore step at the declared target', () => {
|
|
54
|
+
// The read that silently returned undefined: `ci init` takes the keystore
|
|
55
|
+
// target out of the properties file it collected in the same run.
|
|
56
|
+
const sm = new SecretsManager(storePath);
|
|
57
|
+
sm.storeSecret('KEYSTORE_PROPERTIES', KEYSTORE_PROPERTIES, 'release');
|
|
58
|
+
const storeFile = sm
|
|
59
|
+
.getSecret('KEYSTORE_PROPERTIES', 'release')
|
|
60
|
+
?.split('\n')
|
|
61
|
+
.find((line) => line.trim().startsWith('storeFile='));
|
|
62
|
+
expect(storeFile).toBe('storeFile=app/customer.jks');
|
|
63
|
+
});
|
|
64
|
+
test('reads back a global value it just stored', () => {
|
|
65
|
+
const sm = new SecretsManager(storePath);
|
|
66
|
+
sm.storeSecret('LOCAL_PROPERTIES', 'BASE_API_URL=x\n');
|
|
67
|
+
expect(sm.getSecret('LOCAL_PROPERTIES')).toBe('BASE_API_URL=x\n');
|
|
68
|
+
expect(sm.getAll()).toHaveProperty('LOCAL_PROPERTIES', 'BASE_API_URL=x\n');
|
|
69
|
+
expect(sm.getAllSecretNames()).toContain('LOCAL_PROPERTIES');
|
|
70
|
+
});
|
|
71
|
+
test('keeps a value scoped to the workflow it was stored for', () => {
|
|
72
|
+
const sm = new SecretsManager(storePath);
|
|
73
|
+
sm.storeSecret('LOCAL_PROPERTIES', 'ci-dynamic', 'primary');
|
|
74
|
+
sm.storeSecret('LOCAL_PROPERTIES', 'prod', 'release');
|
|
75
|
+
expect(sm.getSecret('LOCAL_PROPERTIES', 'primary')).toBe('ci-dynamic');
|
|
76
|
+
expect(sm.getSecret('LOCAL_PROPERTIES', 'release')).toBe('prod');
|
|
77
|
+
// No global value was stored, so a workflow nobody stored for has none.
|
|
78
|
+
expect(sm.getSecret('LOCAL_PROPERTIES', 'pull-request')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('the environment fallback', () => {
|
|
82
|
+
test('still answers for a key the store does not hold', () => {
|
|
83
|
+
process.env.CIBUILD_FROM_THE_ENVIRONMENT = 'inherited';
|
|
84
|
+
const sm = new SecretsManager(storePath);
|
|
85
|
+
sm.storeSecret('LOCAL_PROPERTIES', 'stored');
|
|
86
|
+
expect(sm.getSecret('FROM_THE_ENVIRONMENT')).toBe('inherited');
|
|
87
|
+
expect(sm.hasSecret('FROM_THE_ENVIRONMENT')).toBe(true);
|
|
88
|
+
expect(sm.getAll()).toMatchObject({
|
|
89
|
+
FROM_THE_ENVIRONMENT: 'inherited',
|
|
90
|
+
LOCAL_PROPERTIES: 'stored',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
test('answers for everything when nothing has been stored', () => {
|
|
94
|
+
process.env.CIBUILD_FROM_THE_ENVIRONMENT = 'inherited';
|
|
95
|
+
const sm = new SecretsManager(storePath);
|
|
96
|
+
expect(sm.getSecret('FROM_THE_ENVIRONMENT')).toBe('inherited');
|
|
97
|
+
expect(sm.getAll()).toEqual({ FROM_THE_ENVIRONMENT: 'inherited' });
|
|
98
|
+
});
|
|
99
|
+
test('loses to a value that was actually stored', () => {
|
|
100
|
+
process.env.CIBUILD_KEYSTORE_PROPERTIES = 'from-the-environment';
|
|
101
|
+
const sm = new SecretsManager(storePath);
|
|
102
|
+
sm.storeSecret('KEYSTORE_PROPERTIES', KEYSTORE_PROPERTIES, 'release');
|
|
103
|
+
expect(sm.getSecret('KEYSTORE_PROPERTIES', 'release')).toBe(KEYSTORE_PROPERTIES);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('a repository that already had a store', () => {
|
|
107
|
+
test('is unchanged — the file is the answer and the environment is not consulted', () => {
|
|
108
|
+
process.env.CIBUILD_FROM_THE_ENVIRONMENT = 'inherited';
|
|
109
|
+
writeFileSync(storePath, JSON.stringify({ global: { A: 'from-the-file' }, workflows: {} }), { mode: 0o600 });
|
|
110
|
+
const sm = new SecretsManager(storePath);
|
|
111
|
+
expect(sm.getSecret('A')).toBe('from-the-file');
|
|
112
|
+
expect(sm.getSecret('FROM_THE_ENVIRONMENT')).toBeUndefined();
|
|
113
|
+
expect(sm.getAll()).toEqual({ A: 'from-the-file' });
|
|
114
|
+
});
|
|
115
|
+
test('still lets a workflow value beat the global one', () => {
|
|
116
|
+
writeFileSync(storePath, JSON.stringify({
|
|
117
|
+
global: { A: 'global' },
|
|
118
|
+
workflows: { release: { A: 'release' } },
|
|
119
|
+
}), { mode: 0o600 });
|
|
120
|
+
const sm = new SecretsManager(storePath);
|
|
121
|
+
expect(sm.getSecret('A', 'release')).toBe('release');
|
|
122
|
+
expect(sm.getSecret('A', 'primary')).toBe('global');
|
|
123
|
+
expect(sm.getAll('release')).toEqual({ A: 'release' });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=a-value-this-instance-stored-is-a-value-it-has.test.js.map
|
|
@@ -15,6 +15,54 @@ export interface ConversionResult {
|
|
|
15
15
|
warnings: string[];
|
|
16
16
|
skippedSteps: number;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A credential a `file@` step writes, and where it goes.
|
|
20
|
+
*/
|
|
21
|
+
export interface CredentialTarget {
|
|
22
|
+
targetPath: string;
|
|
23
|
+
varName: string;
|
|
24
|
+
}
|
|
25
|
+
/** A cache step's declared paths, and which step declared them. */
|
|
26
|
+
export interface CachePathDeclaration {
|
|
27
|
+
stepName: string;
|
|
28
|
+
cachePaths: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SwallowedCredential {
|
|
31
|
+
stepName: string;
|
|
32
|
+
cachePath: string;
|
|
33
|
+
targetPath: string;
|
|
34
|
+
varName: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when caching `cachePath` would sweep up the file at `targetPath`.
|
|
38
|
+
*
|
|
39
|
+
* Only a project-relative entry can: `cache.ts` classifies a `~`- or
|
|
40
|
+
* `/`-rooted entry as absolute and stores it from the filesystem root, so it
|
|
41
|
+
* is somewhere the checkout is not. An entry naming the checkout itself (`.`,
|
|
42
|
+
* `./`) contains everything.
|
|
43
|
+
*/
|
|
44
|
+
export declare function cacheEntryContains(cachePath: string, targetPath: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Every credential a workflow's cache steps would archive.
|
|
47
|
+
*
|
|
48
|
+
* `cache-push` uploads whatever `cache_paths` names, and that archive is
|
|
49
|
+
* stored where the *next* build pulls it from — so a credential swept into it
|
|
50
|
+
* outlives the build that received it. Everything else about a credential's
|
|
51
|
+
* life is bounded: the runner pushes the checkout and the guest root onto
|
|
52
|
+
* `cleanupPaths` and removes them in a `finally`, the guest gets
|
|
53
|
+
* `rm -rf`, and `tart delete` reclaims the VM. The cache store is the one
|
|
54
|
+
* place a file can survive all of that, which makes this the single hole in
|
|
55
|
+
* "nothing survives a build".
|
|
56
|
+
*
|
|
57
|
+
* No pipeline in play trips it — every one of them caches `~/.gradle/caches`,
|
|
58
|
+
* `~/.gradle/wrapper` and `.gradle`, and none of those contains a credential —
|
|
59
|
+
* so this is a guard on a mistake that is easy to make and impossible to see:
|
|
60
|
+
* adding `.` to `cache_paths` is a plausible way to speed up a build, and it
|
|
61
|
+
* would publish the keystore.
|
|
62
|
+
*/
|
|
63
|
+
export declare function credentialsSwallowedByCache(credentials: readonly CredentialTarget[], caches: readonly CachePathDeclaration[]): SwallowedCredential[];
|
|
64
|
+
/** The refusal text for one swallowed credential. */
|
|
65
|
+
export declare function swallowedCredentialMessage(workflowName: string, swallowed: SwallowedCredential): string;
|
|
18
66
|
/**
|
|
19
67
|
* Converts a YAML pipeline workflow to PipelineDef format
|
|
20
68
|
* @param pipeline Parsed YAML pipeline
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAMzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAMzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAaD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CASjF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,SAAS,gBAAgB,EAAE,EACxC,MAAM,EAAE,SAAS,oBAAoB,EAAE,GACtC,mBAAmB,EAAE,CAgBvB;AAED,qDAAqD;AACrD,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,mBAAmB,GAC7B,MAAM,CAaR;AAkCD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAyM3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
|
|
@@ -21,6 +21,87 @@ const CACHE_STEPS = new Set(['cache-pull', 'cache-push']);
|
|
|
21
21
|
function isCacheStep(stepName) {
|
|
22
22
|
return CACHE_STEPS.has(stepName);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Trailing slashes and a leading `./` off, backslashes forward. `.` and the
|
|
26
|
+
* empty string both mean "the whole checkout".
|
|
27
|
+
*/
|
|
28
|
+
function normalizeCheckoutPath(path) {
|
|
29
|
+
const forward = path.trim().replace(/\\/g, '/');
|
|
30
|
+
const withoutLead = forward.replace(/^\.\//, '');
|
|
31
|
+
const withoutTrail = withoutLead.replace(/\/+$/, '');
|
|
32
|
+
return withoutTrail === '.' ? '' : withoutTrail;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when caching `cachePath` would sweep up the file at `targetPath`.
|
|
36
|
+
*
|
|
37
|
+
* Only a project-relative entry can: `cache.ts` classifies a `~`- or
|
|
38
|
+
* `/`-rooted entry as absolute and stores it from the filesystem root, so it
|
|
39
|
+
* is somewhere the checkout is not. An entry naming the checkout itself (`.`,
|
|
40
|
+
* `./`) contains everything.
|
|
41
|
+
*/
|
|
42
|
+
export function cacheEntryContains(cachePath, targetPath) {
|
|
43
|
+
const raw = cachePath.trim();
|
|
44
|
+
if (raw.startsWith('~') || raw.startsWith('/'))
|
|
45
|
+
return false;
|
|
46
|
+
const cache = normalizeCheckoutPath(raw);
|
|
47
|
+
const target = normalizeCheckoutPath(targetPath);
|
|
48
|
+
if (target === '')
|
|
49
|
+
return false;
|
|
50
|
+
if (cache === '')
|
|
51
|
+
return true;
|
|
52
|
+
return target === cache || target.startsWith(`${cache}/`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every credential a workflow's cache steps would archive.
|
|
56
|
+
*
|
|
57
|
+
* `cache-push` uploads whatever `cache_paths` names, and that archive is
|
|
58
|
+
* stored where the *next* build pulls it from — so a credential swept into it
|
|
59
|
+
* outlives the build that received it. Everything else about a credential's
|
|
60
|
+
* life is bounded: the runner pushes the checkout and the guest root onto
|
|
61
|
+
* `cleanupPaths` and removes them in a `finally`, the guest gets
|
|
62
|
+
* `rm -rf`, and `tart delete` reclaims the VM. The cache store is the one
|
|
63
|
+
* place a file can survive all of that, which makes this the single hole in
|
|
64
|
+
* "nothing survives a build".
|
|
65
|
+
*
|
|
66
|
+
* No pipeline in play trips it — every one of them caches `~/.gradle/caches`,
|
|
67
|
+
* `~/.gradle/wrapper` and `.gradle`, and none of those contains a credential —
|
|
68
|
+
* so this is a guard on a mistake that is easy to make and impossible to see:
|
|
69
|
+
* adding `.` to `cache_paths` is a plausible way to speed up a build, and it
|
|
70
|
+
* would publish the keystore.
|
|
71
|
+
*/
|
|
72
|
+
export function credentialsSwallowedByCache(credentials, caches) {
|
|
73
|
+
const found = [];
|
|
74
|
+
for (const cache of caches) {
|
|
75
|
+
for (const cachePath of cache.cachePaths) {
|
|
76
|
+
for (const credential of credentials) {
|
|
77
|
+
if (!cacheEntryContains(cachePath, credential.targetPath))
|
|
78
|
+
continue;
|
|
79
|
+
found.push({
|
|
80
|
+
stepName: cache.stepName,
|
|
81
|
+
cachePath: cachePath.trim(),
|
|
82
|
+
targetPath: credential.targetPath,
|
|
83
|
+
varName: credential.varName,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
/** The refusal text for one swallowed credential. */
|
|
91
|
+
export function swallowedCredentialMessage(workflowName, swallowed) {
|
|
92
|
+
return [
|
|
93
|
+
`Workflow "${workflowName}" would cache a credential.`,
|
|
94
|
+
``,
|
|
95
|
+
` The ${swallowed.stepName} step lists "${swallowed.cachePath}" in cache_paths, and that`,
|
|
96
|
+
` contains "${swallowed.targetPath}" — the file written from the ${swallowed.varName} secret.`,
|
|
97
|
+
``,
|
|
98
|
+
` A cached credential outlives the build. The archive is stored where the next`,
|
|
99
|
+
` build pulls it from, and nothing removes it when the sandbox is destroyed.`,
|
|
100
|
+
``,
|
|
101
|
+
` Narrow the cache_paths entry to the directories the build needs cached, or`,
|
|
102
|
+
` write the credential somewhere outside them.`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
24
105
|
/**
|
|
25
106
|
* Mapping of common unsupported steps to CI Build alternatives
|
|
26
107
|
*/
|
|
@@ -98,6 +179,11 @@ export async function convertYAMLToPipelineDef(pipeline, config, workflowName, y
|
|
|
98
179
|
const steps = [];
|
|
99
180
|
const stepsOrder = [];
|
|
100
181
|
let stepCounter = 0;
|
|
182
|
+
// Collected as the steps are converted, checked once at the end: a cache
|
|
183
|
+
// step can be declared before the `file@` step whose target it would
|
|
184
|
+
// swallow, so neither one alone can answer the question.
|
|
185
|
+
const credentialTargets = [];
|
|
186
|
+
const cacheDeclarations = [];
|
|
101
187
|
for (const yamlStep of workflowSteps) {
|
|
102
188
|
// Parse step (sub-task 3.8)
|
|
103
189
|
const parsedStep = parseStep(yamlStep, stepCounter);
|
|
@@ -154,6 +240,27 @@ export async function convertYAMLToPipelineDef(pipeline, config, workflowName, y
|
|
|
154
240
|
}
|
|
155
241
|
throw error;
|
|
156
242
|
}
|
|
243
|
+
// Both halves of the cache-swallows-a-credential question, read off the
|
|
244
|
+
// interpolated inputs so `$PROJECT_LOCATION` and friends are already the
|
|
245
|
+
// paths they will be at runtime.
|
|
246
|
+
if (resolvedStepName === 'file') {
|
|
247
|
+
const targetPath = interpolatedInputs?.target_path;
|
|
248
|
+
const varName = interpolatedInputs?.var_name;
|
|
249
|
+
if (typeof targetPath === 'string' && typeof varName === 'string') {
|
|
250
|
+
credentialTargets.push({ targetPath, varName });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (isCacheStep(resolvedStepName)) {
|
|
254
|
+
const declared = interpolatedInputs?.cache_paths;
|
|
255
|
+
const cachePaths = Array.isArray(declared)
|
|
256
|
+
? declared.filter((p) => typeof p === 'string')
|
|
257
|
+
: typeof declared === 'string'
|
|
258
|
+
? [declared]
|
|
259
|
+
: [];
|
|
260
|
+
if (cachePaths.length > 0) {
|
|
261
|
+
cacheDeclarations.push({ stepName: resolvedStepName, cachePaths });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
157
264
|
// Execute step to get script
|
|
158
265
|
const stepDef = await stepExecutor.execute(interpolatedInputs, envResolver.getAll(), config);
|
|
159
266
|
// Generate unique step ID
|
|
@@ -174,6 +281,16 @@ export async function convertYAMLToPipelineDef(pipeline, config, workflowName, y
|
|
|
174
281
|
}
|
|
175
282
|
}
|
|
176
283
|
}
|
|
284
|
+
// Step 4b: refuse a pipeline that would archive a credential.
|
|
285
|
+
//
|
|
286
|
+
// Thrown at generation time for the same reason issue 03's missing-credential
|
|
287
|
+
// refusal is: it is the only place that can see the whole workflow, and the
|
|
288
|
+
// alternative is a build that succeeds while publishing a key. A pipeline
|
|
289
|
+
// configuration error, and the author is the one who can fix it.
|
|
290
|
+
const swallowed = credentialsSwallowedByCache(credentialTargets, cacheDeclarations);
|
|
291
|
+
if (swallowed.length > 0) {
|
|
292
|
+
throw new YAMLConversionError(swallowed.map((s) => swallowedCredentialMessage(selectedWorkflowName, s)).join('\n\n'));
|
|
293
|
+
}
|
|
177
294
|
// Step 5: Create PipelineDef
|
|
178
295
|
const pipelineDef = {
|
|
179
296
|
name: `${selectedWorkflowName} (YAML)`,
|
|
@@ -23,6 +23,30 @@ export declare class ChecksumFileMissingError extends Error {
|
|
|
23
23
|
readonly stepName?: string | undefined;
|
|
24
24
|
constructor(filePath: string, stepName?: string | undefined);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A credential slot scoped to a workflow the pipeline does not have.
|
|
28
|
+
*
|
|
29
|
+
* Thrown at generation time, before any step converts, because every way of
|
|
30
|
+
* getting the name wrong is otherwise silent. A slot naming a workflow nobody
|
|
31
|
+
* runs declares the credential for nobody: `file@1.0.0` finds no key in its
|
|
32
|
+
* map, calls the credential optional and skips it with a warning, and
|
|
33
|
+
* `ci secrets upload` cheerfully pushes a value under the same wrong name.
|
|
34
|
+
* Two green halves and a build that dies later inside the project's own build
|
|
35
|
+
* tool.
|
|
36
|
+
*
|
|
37
|
+
* The underscore case gets its own sentence because the workflow is right
|
|
38
|
+
* there in the pipeline. `encodeSecretName` upper-cases the workflow and turns
|
|
39
|
+
* dashes into underscores, so decoding turns every underscore back into a
|
|
40
|
+
* dash: a workflow genuinely called `my_wf` can only ever be addressed as
|
|
41
|
+
* `my-wf`, and "no such workflow" about a workflow that plainly exists reads
|
|
42
|
+
* as a defect in us rather than a name that cannot survive the round trip.
|
|
43
|
+
*/
|
|
44
|
+
export declare class UnknownWorkflowSlotError extends Error {
|
|
45
|
+
readonly slot: string;
|
|
46
|
+
readonly varName: string;
|
|
47
|
+
readonly workflow: string;
|
|
48
|
+
constructor(slot: string, varName: string, workflow: string, declared: string[]);
|
|
49
|
+
}
|
|
26
50
|
/**
|
|
27
51
|
* EnvResolver handles environment variable merging and interpolation
|
|
28
52
|
*/
|
|
@@ -35,6 +59,10 @@ export declare class EnvResolver {
|
|
|
35
59
|
* not it gave one a value. Read by `importDeclaredFromProcessEnv`.
|
|
36
60
|
*/
|
|
37
61
|
private declaredVars;
|
|
62
|
+
/** The workflow being resolved. Decides which scoped slots apply — `slotKeyFor`. */
|
|
63
|
+
private workflowName;
|
|
64
|
+
/** Every workflow this pipeline declares. A scoped slot has to name one of them. */
|
|
65
|
+
private declaredWorkflows;
|
|
38
66
|
constructor(pipeline: YAMLPipeline, workflow: YAMLWorkflow, workflowName: string, platform: Platform, stack?: string, yamlFilePath?: string);
|
|
39
67
|
/**
|
|
40
68
|
* Gets the value of an environment variable
|
|
@@ -111,6 +139,30 @@ export declare class EnvResolver {
|
|
|
111
139
|
* Merges environment variables from YAML array format
|
|
112
140
|
* @param envArray Array of environment variable objects
|
|
113
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* The variable a declared `envs` key stands for, or null when the
|
|
144
|
+
* declaration belongs to a different workflow.
|
|
145
|
+
*
|
|
146
|
+
* A credential two workflows cannot share is declared once per workflow,
|
|
147
|
+
* under the same name a workflow-scoped secret carries — the pipeline says
|
|
148
|
+
* `CIBUILD_SW__RELEASE__LOCAL_PROPERTIES: ""` where it used to say
|
|
149
|
+
* `LOCAL_PROPERTIES: ""`. The build still reads the credential under its
|
|
150
|
+
* plain name, so the declaration has to mean the plain name: it is what puts
|
|
151
|
+
* an unsupplied key in the map for `file@1.0.0` to refuse over, and what
|
|
152
|
+
* lets `importDeclaredFromProcessEnv` accept a value delivered flat.
|
|
153
|
+
*
|
|
154
|
+
* A slot naming another workflow is dropped rather than declared. Declaring
|
|
155
|
+
* it would make this workflow demand a value only that one was ever going to
|
|
156
|
+
* be given. The decoding lives here, beside the decoding of the secret
|
|
157
|
+
* itself, so the steps keep reading their plain `var_name` and never learn
|
|
158
|
+
* the convention exists.
|
|
159
|
+
*
|
|
160
|
+
* A slot naming a workflow the pipeline does not have is refused rather than
|
|
161
|
+
* dropped — see `UnknownWorkflowSlotError`. Dropping it silently is the same
|
|
162
|
+
* outcome as never declaring the credential, which is the green skip issue 03
|
|
163
|
+
* exists to end.
|
|
164
|
+
*/
|
|
165
|
+
private slotKeyFor;
|
|
114
166
|
private mergeEnvVars;
|
|
115
167
|
/**
|
|
116
168
|
* Fills a declared-but-still-empty variable from the process environment.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkEnF,qBAAa,+BAAgC,SAAQ,KAAK;aAEtC,YAAY,EAAE,MAAM;aACpB,QAAQ,CAAC,EAAE,MAAM;aACjB,IAAI,CAAC,EAAE,MAAM;gBAFb,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,YAAA,EACjB,IAAI,CAAC,EAAE,MAAM,YAAA;CAUhC;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,QAAQ,EAAE,MAAM;aAChB,QAAQ,CAAC,EAAE,MAAM;gBADjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAKpC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;CAgChF;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAiB;IACvC;;;OAGG;IACH,OAAO,CAAC,YAAY,CAA0B;IAC9C,oFAAoF;IACpF,OAAO,CAAC,YAAY,CAAS;IAC7B,oFAAoF;IACpF,OAAO,CAAC,iBAAiB,CAAW;gBAGlC,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM;IAoCvB;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKrC;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMtC;;;OAGG;IACH,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQhC;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAqCnD;;;;;OAKG;IACH,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG;IAoBnD;;;OAGG;IACH,sBAAsB,IAAI,IAAI;IAQ9B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAalB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IA4FtB;;;OAGG;IACH;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,UAAU;IAiBlB,OAAO,CAAC,YAAY;IAsBpB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAS5B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,WAAW,CAOb"}
|
|
@@ -8,6 +8,7 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
import { SecretsManager } from './secrets-manager.js';
|
|
11
|
+
import { decodeSecretName } from '../commands/secrets-upload.js';
|
|
11
12
|
import { shellAssignedNamesIn } from './shell-assignments.js';
|
|
12
13
|
/**
|
|
13
14
|
* Hints for common variables that require user configuration
|
|
@@ -95,6 +96,58 @@ export class ChecksumFileMissingError extends Error {
|
|
|
95
96
|
this.name = 'ChecksumFileMissingError';
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* A credential slot scoped to a workflow the pipeline does not have.
|
|
101
|
+
*
|
|
102
|
+
* Thrown at generation time, before any step converts, because every way of
|
|
103
|
+
* getting the name wrong is otherwise silent. A slot naming a workflow nobody
|
|
104
|
+
* runs declares the credential for nobody: `file@1.0.0` finds no key in its
|
|
105
|
+
* map, calls the credential optional and skips it with a warning, and
|
|
106
|
+
* `ci secrets upload` cheerfully pushes a value under the same wrong name.
|
|
107
|
+
* Two green halves and a build that dies later inside the project's own build
|
|
108
|
+
* tool.
|
|
109
|
+
*
|
|
110
|
+
* The underscore case gets its own sentence because the workflow is right
|
|
111
|
+
* there in the pipeline. `encodeSecretName` upper-cases the workflow and turns
|
|
112
|
+
* dashes into underscores, so decoding turns every underscore back into a
|
|
113
|
+
* dash: a workflow genuinely called `my_wf` can only ever be addressed as
|
|
114
|
+
* `my-wf`, and "no such workflow" about a workflow that plainly exists reads
|
|
115
|
+
* as a defect in us rather than a name that cannot survive the round trip.
|
|
116
|
+
*/
|
|
117
|
+
export class UnknownWorkflowSlotError extends Error {
|
|
118
|
+
slot;
|
|
119
|
+
varName;
|
|
120
|
+
workflow;
|
|
121
|
+
constructor(slot, varName, workflow, declared) {
|
|
122
|
+
// A workflow the pipeline has that differs only in its underscores is the
|
|
123
|
+
// reason the round trip failed, and naming it is the whole fix.
|
|
124
|
+
const collapsed = declared.find((name) => name.replace(/_/g, '-') === workflow);
|
|
125
|
+
const because = collapsed
|
|
126
|
+
? `This pipeline has a workflow called "${collapsed}". The name a slot ` +
|
|
127
|
+
`carries cannot hold an underscore: it is upper-cased and its dashes ` +
|
|
128
|
+
`become underscores, so decoding turns every underscore back into a ` +
|
|
129
|
+
`dash and "${collapsed}" can only be addressed as "${workflow}".\n` +
|
|
130
|
+
`\n` +
|
|
131
|
+
`Rename the workflow to "${workflow}", or give ${varName} one value ` +
|
|
132
|
+
`for every workflow by declaring it as "${varName}".`
|
|
133
|
+
: `This pipeline declares: ${declared.join(', ')}.\n` +
|
|
134
|
+
`\n` +
|
|
135
|
+
`Fix the workflow name in the slot, or give ${varName} one value for ` +
|
|
136
|
+
`every workflow by declaring it as "${varName}".`;
|
|
137
|
+
super(`${slot} is declared for a workflow this pipeline does not have.\n` +
|
|
138
|
+
`\n` +
|
|
139
|
+
`The slot asks for ${varName}, for a workflow called "${workflow}". ` +
|
|
140
|
+
`Nothing runs under that name, so nothing would ever read the value: ` +
|
|
141
|
+
`the step that writes ${varName} would find no value and skip, and the ` +
|
|
142
|
+
`build would fail later for a reason that names your project.\n` +
|
|
143
|
+
`\n` +
|
|
144
|
+
`${because}`);
|
|
145
|
+
this.name = 'UnknownWorkflowSlotError';
|
|
146
|
+
this.slot = slot;
|
|
147
|
+
this.varName = varName;
|
|
148
|
+
this.workflow = workflow;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
98
151
|
/**
|
|
99
152
|
* EnvResolver handles environment variable merging and interpolation
|
|
100
153
|
*/
|
|
@@ -107,9 +160,15 @@ export class EnvResolver {
|
|
|
107
160
|
* not it gave one a value. Read by `importDeclaredFromProcessEnv`.
|
|
108
161
|
*/
|
|
109
162
|
declaredVars = new Set();
|
|
163
|
+
/** The workflow being resolved. Decides which scoped slots apply — `slotKeyFor`. */
|
|
164
|
+
workflowName;
|
|
165
|
+
/** Every workflow this pipeline declares. A scoped slot has to name one of them. */
|
|
166
|
+
declaredWorkflows;
|
|
110
167
|
constructor(pipeline, workflow, workflowName, platform, stack, yamlFilePath) {
|
|
111
168
|
this.envVars = new Map();
|
|
112
169
|
this.secretsManager = new SecretsManager();
|
|
170
|
+
this.workflowName = workflowName;
|
|
171
|
+
this.declaredWorkflows = Object.keys(pipeline.workflows ?? {});
|
|
113
172
|
// Step 1: Add built-in environment variables (auto-detected defaults)
|
|
114
173
|
this.addBuiltInVars(workflowName, platform, stack, yamlFilePath);
|
|
115
174
|
// Step 2: Load secrets — overrides auto-detected built-ins with user-stored values
|
|
@@ -378,9 +437,48 @@ export class EnvResolver {
|
|
|
378
437
|
* Merges environment variables from YAML array format
|
|
379
438
|
* @param envArray Array of environment variable objects
|
|
380
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* The variable a declared `envs` key stands for, or null when the
|
|
442
|
+
* declaration belongs to a different workflow.
|
|
443
|
+
*
|
|
444
|
+
* A credential two workflows cannot share is declared once per workflow,
|
|
445
|
+
* under the same name a workflow-scoped secret carries — the pipeline says
|
|
446
|
+
* `CIBUILD_SW__RELEASE__LOCAL_PROPERTIES: ""` where it used to say
|
|
447
|
+
* `LOCAL_PROPERTIES: ""`. The build still reads the credential under its
|
|
448
|
+
* plain name, so the declaration has to mean the plain name: it is what puts
|
|
449
|
+
* an unsupplied key in the map for `file@1.0.0` to refuse over, and what
|
|
450
|
+
* lets `importDeclaredFromProcessEnv` accept a value delivered flat.
|
|
451
|
+
*
|
|
452
|
+
* A slot naming another workflow is dropped rather than declared. Declaring
|
|
453
|
+
* it would make this workflow demand a value only that one was ever going to
|
|
454
|
+
* be given. The decoding lives here, beside the decoding of the secret
|
|
455
|
+
* itself, so the steps keep reading their plain `var_name` and never learn
|
|
456
|
+
* the convention exists.
|
|
457
|
+
*
|
|
458
|
+
* A slot naming a workflow the pipeline does not have is refused rather than
|
|
459
|
+
* dropped — see `UnknownWorkflowSlotError`. Dropping it silently is the same
|
|
460
|
+
* outcome as never declaring the credential, which is the green skip issue 03
|
|
461
|
+
* exists to end.
|
|
462
|
+
*/
|
|
463
|
+
slotKeyFor(key) {
|
|
464
|
+
const decoded = decodeSecretName(key);
|
|
465
|
+
if (!decoded)
|
|
466
|
+
return key;
|
|
467
|
+
if (decoded.workflow) {
|
|
468
|
+
if (!this.declaredWorkflows.includes(decoded.workflow)) {
|
|
469
|
+
throw new UnknownWorkflowSlotError(key, decoded.varName, decoded.workflow, this.declaredWorkflows);
|
|
470
|
+
}
|
|
471
|
+
if (decoded.workflow !== this.workflowName)
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return decoded.varName;
|
|
475
|
+
}
|
|
381
476
|
mergeEnvVars(envArray) {
|
|
382
477
|
for (const envObj of envArray) {
|
|
383
|
-
for (const [
|
|
478
|
+
for (const [declaredKey, value] of Object.entries(envObj)) {
|
|
479
|
+
const key = this.slotKeyFor(declaredKey);
|
|
480
|
+
if (key === null)
|
|
481
|
+
continue;
|
|
384
482
|
// Naming the key is the declaration, whatever value follows it
|
|
385
483
|
this.declaredVars.add(key);
|
|
386
484
|
// First resolve any secret references
|
|
@@ -43,17 +43,27 @@ export declare class SecretsManager {
|
|
|
43
43
|
* Get a secret value by variable name.
|
|
44
44
|
* Workflow-specific value takes precedence over global.
|
|
45
45
|
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
|
|
46
|
+
*
|
|
47
|
+
* The stored data is consulted before that fallback, and not only when a
|
|
48
|
+
* file was loaded. `storeSecret` writes the file but cannot make it one this
|
|
49
|
+
* instance *loaded*, so a value collected by `ci init` on a repository that
|
|
50
|
+
* had no store — which is the only kind of run that collects anything — read
|
|
51
|
+
* back as absent from the instance that had just stored it. `ci init`'s own
|
|
52
|
+
* `getSecret('KEYSTORE_PROPERTIES', wf)`, which is how a workflow's keystore
|
|
53
|
+
* target is taken from `storeFile=`, returned undefined on every fresh
|
|
54
|
+
* repository because of it.
|
|
46
55
|
*/
|
|
47
56
|
getSecret(name: string, workflow?: string): string | undefined;
|
|
48
57
|
/**
|
|
49
58
|
* Get all secrets merged: global + workflow-specific (workflow overrides global).
|
|
50
|
-
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present
|
|
59
|
+
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present —
|
|
60
|
+
* for the keys the store does not hold. See `getSecret`.
|
|
51
61
|
*/
|
|
52
62
|
getAll(workflow?: string): Record<string, string>;
|
|
53
63
|
/**
|
|
54
64
|
* Returns the variable name if a secret exists for it, undefined otherwise.
|
|
55
|
-
* Checks workflow scope first, then global
|
|
56
|
-
*
|
|
65
|
+
* Checks workflow scope first, then global, then the CIBUILD_-prefixed env
|
|
66
|
+
* vars when no secrets file is present. See `getSecret`.
|
|
57
67
|
*/
|
|
58
68
|
getSecretIdByName(name: string, workflow?: string): string | undefined;
|
|
59
69
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets-manager.d.ts","sourceRoot":"","sources":["../../../src/yaml/secrets-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,MAAM;IAMpC,OAAO,CAAC,WAAW;IAqCnB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,WAAW;IAUnB;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAanE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB
|
|
1
|
+
{"version":3,"file":"secrets-manager.d.ts","sourceRoot":"","sources":["../../../src/yaml/secrets-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,MAAM;IAMpC,OAAO,CAAC,WAAW;IAqCnB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,WAAW;IAUnB;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAanE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAa9D;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASjD;;;;OAIG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAatE;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAInD;;OAEG;IACH,iBAAiB,IAAI,MAAM,EAAE;IAQ7B;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAetD,kBAAkB,IAAI,MAAM;CAG7B"}
|
|
@@ -129,25 +129,36 @@ export class SecretsManager {
|
|
|
129
129
|
* Get a secret value by variable name.
|
|
130
130
|
* Workflow-specific value takes precedence over global.
|
|
131
131
|
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
|
|
132
|
+
*
|
|
133
|
+
* The stored data is consulted before that fallback, and not only when a
|
|
134
|
+
* file was loaded. `storeSecret` writes the file but cannot make it one this
|
|
135
|
+
* instance *loaded*, so a value collected by `ci init` on a repository that
|
|
136
|
+
* had no store — which is the only kind of run that collects anything — read
|
|
137
|
+
* back as absent from the instance that had just stored it. `ci init`'s own
|
|
138
|
+
* `getSecret('KEYSTORE_PROPERTIES', wf)`, which is how a workflow's keystore
|
|
139
|
+
* target is taken from `storeFile=`, returned undefined on every fresh
|
|
140
|
+
* repository because of it.
|
|
132
141
|
*/
|
|
133
142
|
getSecret(name, workflow) {
|
|
134
|
-
if (!this.fileLoaded) {
|
|
135
|
-
return process.env[SecretsManager.ENV_PREFIX + name];
|
|
136
|
-
}
|
|
137
143
|
if (workflow && this.data.workflows[workflow]?.[name] !== undefined) {
|
|
138
144
|
return this.data.workflows[workflow][name];
|
|
139
145
|
}
|
|
140
|
-
|
|
146
|
+
if (this.data.global[name] !== undefined) {
|
|
147
|
+
return this.data.global[name];
|
|
148
|
+
}
|
|
149
|
+
if (!this.fileLoaded) {
|
|
150
|
+
return process.env[SecretsManager.ENV_PREFIX + name];
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
155
|
* Get all secrets merged: global + workflow-specific (workflow overrides global).
|
|
144
|
-
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present
|
|
156
|
+
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present —
|
|
157
|
+
* for the keys the store does not hold. See `getSecret`.
|
|
145
158
|
*/
|
|
146
159
|
getAll(workflow) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const merged = { ...this.data.global };
|
|
160
|
+
const merged = this.fileLoaded ? {} : this.getEnvSecrets();
|
|
161
|
+
Object.assign(merged, this.data.global);
|
|
151
162
|
if (workflow && this.data.workflows[workflow]) {
|
|
152
163
|
Object.assign(merged, this.data.workflows[workflow]);
|
|
153
164
|
}
|
|
@@ -155,19 +166,19 @@ export class SecretsManager {
|
|
|
155
166
|
}
|
|
156
167
|
/**
|
|
157
168
|
* Returns the variable name if a secret exists for it, undefined otherwise.
|
|
158
|
-
* Checks workflow scope first, then global
|
|
159
|
-
*
|
|
169
|
+
* Checks workflow scope first, then global, then the CIBUILD_-prefixed env
|
|
170
|
+
* vars when no secrets file is present. See `getSecret`.
|
|
160
171
|
*/
|
|
161
172
|
getSecretIdByName(name, workflow) {
|
|
162
|
-
if (!this.fileLoaded) {
|
|
163
|
-
return process.env[SecretsManager.ENV_PREFIX + name] !== undefined ? name : undefined;
|
|
164
|
-
}
|
|
165
173
|
if (workflow && this.data.workflows[workflow]?.[name] !== undefined) {
|
|
166
174
|
return name;
|
|
167
175
|
}
|
|
168
176
|
if (this.data.global[name] !== undefined) {
|
|
169
177
|
return name;
|
|
170
178
|
}
|
|
179
|
+
if (!this.fileLoaded) {
|
|
180
|
+
return process.env[SecretsManager.ENV_PREFIX + name] !== undefined ? name : undefined;
|
|
181
|
+
}
|
|
171
182
|
return undefined;
|
|
172
183
|
}
|
|
173
184
|
/**
|