@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.
Files changed (52) hide show
  1. package/dist/cli.cjs +10 -10
  2. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
  3. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
  4. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +219 -0
  5. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
  6. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
  7. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +291 -0
  8. package/dist/src/commands/build.d.ts +124 -1
  9. package/dist/src/commands/build.d.ts.map +1 -1
  10. package/dist/src/commands/build.js +251 -33
  11. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
  12. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
  13. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
  14. package/dist/src/commands/file-secret-collector.d.ts +13 -0
  15. package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
  16. package/dist/src/commands/file-secret-collector.js +3 -2
  17. package/dist/src/commands/local-properties-reaches-every-workflow.test.js +2 -0
  18. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
  19. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
  20. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
  21. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
  22. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
  23. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
  24. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
  25. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
  26. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
  27. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
  28. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
  29. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
  30. package/dist/src/yaml/converter.d.ts +48 -0
  31. package/dist/src/yaml/converter.d.ts.map +1 -1
  32. package/dist/src/yaml/converter.js +117 -0
  33. package/dist/src/yaml/env-resolver.d.ts +52 -0
  34. package/dist/src/yaml/env-resolver.d.ts.map +1 -1
  35. package/dist/src/yaml/env-resolver.js +99 -1
  36. package/dist/src/yaml/secrets-manager.d.ts +13 -3
  37. package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
  38. package/dist/src/yaml/secrets-manager.js +25 -14
  39. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
  40. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
  41. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
  42. package/dist/src/yaml/steps/index.d.ts.map +1 -1
  43. package/dist/src/yaml/steps/index.js +9 -0
  44. package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
  45. package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
  46. package/dist/src/yaml/steps/ios-deps.js +18 -3
  47. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
  48. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
  49. package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
  50. package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
  51. package/dist/src/yaml/steps/xcode.js +73 -20
  52. 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;AA6CD;;;;;;;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,CAgK3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
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;AAiEnF,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;;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;gBAG5C,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;IAkCvB;;;;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,OAAO,CAAC,YAAY;IAoBpB;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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 [key, value] of Object.entries(envObj)) {
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
- * Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
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;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAU9D;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAWjD;;;;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"}
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
- return this.data.global[name];
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
- if (!this.fileLoaded) {
148
- return this.getEnvSecrets();
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
- * Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
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
  /**