@invarn/cibuild 2.6.8 → 2.7.0

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.
@@ -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;AAgEnF,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;gBAGrC,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;IA0BvB;;;;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;IA0BnD;;;;;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;IAkBpB;;;;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;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;gBAGrC,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;IA0BvB;;;;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;IAkBpB;;;;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 { shellAssignedNamesIn } from './shell-assignments.js';
11
12
  /**
12
13
  * Hints for common variables that require user configuration
13
14
  */
@@ -174,21 +175,31 @@ export class EnvResolver {
174
175
  */
175
176
  interpolate(str, stepName) {
176
177
  let result = str;
178
+ // A name this very string assigns is the shell's own, and substituting it
179
+ // is worse than demanding it: `IOS_DIR=$(dirname "$IOS_PROJECT_PATH")`
180
+ // followed by `"$IOS_DIR"` would get a pipeline value pasted in instead of
181
+ // the one `dirname` had just computed, so a defined variable would corrupt
182
+ // the script and an undefined one would refuse the build. Left for the
183
+ // shell, which is the only thing that can evaluate it. Only the assignment
184
+ // target is spared — the right-hand side is still resolved here.
185
+ const selfAssigned = shellAssignedNamesIn(str);
177
186
  // Format 1: {{checksum "file"}}
178
187
  result = result.replace(/\{\{checksum "([^"]+)"\}\}/g, (match, filePath) => {
179
188
  return this.computeChecksum(filePath, stepName);
180
189
  });
181
- // Format 2: {{getenv "VAR"}}
190
+ // Format 2: {{getenv "VAR"}} — an explicit request, so it is honoured even
191
+ // where the snippet also assigns that name. Asking by name is not the same
192
+ // as a bare `$VAR` the shell was always going to expand itself.
182
193
  result = result.replace(/\{\{getenv "([^"]+)"\}\}/g, (match, varName) => {
183
194
  return this.resolveVar(varName, stepName);
184
195
  });
185
196
  // Format 3: ${VAR}
186
197
  result = result.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, varName) => {
187
- return this.resolveVar(varName, stepName);
198
+ return selfAssigned.has(varName) ? match : this.resolveVar(varName, stepName);
188
199
  });
189
200
  // Format 4: $VAR (must not be followed by alphanumeric or underscore)
190
201
  result = result.replace(/\$([A-Z_][A-Z0-9_]*)(?![A-Z0-9_])/g, (match, varName) => {
191
- return this.resolveVar(varName, stepName);
202
+ return selfAssigned.has(varName) ? match : this.resolveVar(varName, stepName);
192
203
  });
193
204
  return result;
194
205
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Which names a shell snippet binds for itself.
3
+ *
4
+ * Two different parts of this codebase scan a step's inputs for `$UPPERCASE`
5
+ * and act on every hit — the pre-execution validator, which demands each one
6
+ * be supplied, and the env resolver, which *substitutes* each one. For a
7
+ * `script` step the inputs are the shell body, so a variable the script
8
+ * computes is indistinguishable from one it needs handed to it. Both were
9
+ * wrong about the same thing, so they share one answer.
10
+ *
11
+ * A generated multiplatform pipeline made it plain. Its prepare step opens
12
+ *
13
+ * INTEGRATION="$KMM_IOS_INTEGRATION"
14
+ * IOS_DIR=$(dirname "$IOS_PROJECT_PATH")
15
+ * for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do …
16
+ *
17
+ * and no build could start: the validator refused for three values nothing was
18
+ * meant to provide, and the resolver would have gone further and *rewritten*
19
+ * the body, replacing `"$IOS_DIR"` with a pipeline value instead of the one
20
+ * `dirname` had just computed. Substituting a shell local is not a missing
21
+ * value at all; it is a corrupted script.
22
+ *
23
+ * **Only the assignment target is collected, never the right-hand side.** That
24
+ * is what keeps this from being a hole: `INTEGRATION="$KMM_IOS_INTEGRATION"`
25
+ * still needs `KMM_IOS_INTEGRATION`, so a script cannot excuse itself from a
26
+ * value it reads by mentioning it.
27
+ *
28
+ * Assignment order is deliberately not modelled. A snippet that reads a
29
+ * variable before setting it is a bug in the snippet, and guessing about it
30
+ * here would trade one wrong answer for another; "does the script set this
31
+ * itself?" is the question worth asking.
32
+ */
33
+ /** The names one snippet of shell assigns to itself. */
34
+ export declare function shellAssignedNamesIn(text: string): Set<string>;
35
+ /**
36
+ * The same question asked of a whole inputs object, walked recursively — what
37
+ * the validator needs, because it reasons about a step rather than a string.
38
+ */
39
+ export declare function shellAssignedNames(obj: unknown): Set<string>;
40
+ //# sourceMappingURL=shell-assignments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-assignments.d.ts","sourceRoot":"","sources":["../../../src/yaml/shell-assignments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAaH,wDAAwD;AACxD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAa9D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAa5D"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Which names a shell snippet binds for itself.
3
+ *
4
+ * Two different parts of this codebase scan a step's inputs for `$UPPERCASE`
5
+ * and act on every hit — the pre-execution validator, which demands each one
6
+ * be supplied, and the env resolver, which *substitutes* each one. For a
7
+ * `script` step the inputs are the shell body, so a variable the script
8
+ * computes is indistinguishable from one it needs handed to it. Both were
9
+ * wrong about the same thing, so they share one answer.
10
+ *
11
+ * A generated multiplatform pipeline made it plain. Its prepare step opens
12
+ *
13
+ * INTEGRATION="$KMM_IOS_INTEGRATION"
14
+ * IOS_DIR=$(dirname "$IOS_PROJECT_PATH")
15
+ * for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do …
16
+ *
17
+ * and no build could start: the validator refused for three values nothing was
18
+ * meant to provide, and the resolver would have gone further and *rewritten*
19
+ * the body, replacing `"$IOS_DIR"` with a pipeline value instead of the one
20
+ * `dirname` had just computed. Substituting a shell local is not a missing
21
+ * value at all; it is a corrupted script.
22
+ *
23
+ * **Only the assignment target is collected, never the right-hand side.** That
24
+ * is what keeps this from being a hole: `INTEGRATION="$KMM_IOS_INTEGRATION"`
25
+ * still needs `KMM_IOS_INTEGRATION`, so a script cannot excuse itself from a
26
+ * value it reads by mentioning it.
27
+ *
28
+ * Assignment order is deliberately not modelled. A snippet that reads a
29
+ * variable before setting it is a bug in the snippet, and guessing about it
30
+ * here would trade one wrong answer for another; "does the script set this
31
+ * itself?" is the question worth asking.
32
+ */
33
+ /** `NAME=…`, `export NAME=…`, `local/readonly/declare/typeset NAME=…`, `NAME+=…` */
34
+ const ASSIGNMENT = /(?:^|[\n;&|(]|\|\||&&)[ \t]*(?:(?:export|readonly|local|declare|typeset)[ \t]+)?([A-Z_][A-Z_0-9]*)\+?=/g;
35
+ /** `for NAME in …`, `select NAME in …` */
36
+ const LOOP = /(?:^|[\n;&|(])[ \t]*(?:for|select)[ \t]+([A-Z_][A-Z_0-9]*)[ \t]+in\b/g;
37
+ /** `read NAME`, `read -r NAME OTHER`, `while read -r NAME` */
38
+ const READ = /(?:^|[\n;&|(]|\|\||&&)[ \t]*(?:while[ \t]+)?read[ \t]+((?:-\S+[ \t]+)*)([A-Z_][A-Z_0-9]*(?:[ \t]+[A-Z_][A-Z_0-9]*)*)/g;
39
+ /** The names one snippet of shell assigns to itself. */
40
+ export function shellAssignedNamesIn(text) {
41
+ const names = new Set();
42
+ for (const re of [ASSIGNMENT, LOOP]) {
43
+ re.lastIndex = 0;
44
+ let m;
45
+ while ((m = re.exec(text)) !== null)
46
+ names.add(m[1]);
47
+ }
48
+ READ.lastIndex = 0;
49
+ let m;
50
+ while ((m = READ.exec(text)) !== null) {
51
+ for (const n of m[2].trim().split(/[ \t]+/))
52
+ names.add(n);
53
+ }
54
+ return names;
55
+ }
56
+ /**
57
+ * The same question asked of a whole inputs object, walked recursively — what
58
+ * the validator needs, because it reasons about a step rather than a string.
59
+ */
60
+ export function shellAssignedNames(obj) {
61
+ const names = new Set();
62
+ const walk = (value) => {
63
+ if (typeof value === 'string') {
64
+ for (const n of shellAssignedNamesIn(value))
65
+ names.add(n);
66
+ }
67
+ else if (Array.isArray(value)) {
68
+ value.forEach(walk);
69
+ }
70
+ else if (value && typeof value === 'object') {
71
+ Object.values(value).forEach(walk);
72
+ }
73
+ };
74
+ walk(obj);
75
+ return names;
76
+ }
77
+ //# sourceMappingURL=shell-assignments.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A step does not owe itself the variables its own shell sets.
3
+ *
4
+ * Pre-execution validation auto-discovers every `$UPPERCASE` in a step's
5
+ * inputs and treats it as a value the pipeline must supply. For a `script`
6
+ * step the inputs *are* the shell body, so a variable the script computes was
7
+ * indistinguishable from one it needed handed to it — and because the check
8
+ * cannot prompt in a sandbox, the pipeline was refused before step 1 for
9
+ * values nothing was ever meant to provide.
10
+ *
11
+ * A generated multiplatform pipeline hit exactly that. Its prepare step opens
12
+ * with `INTEGRATION="$KMM_IOS_INTEGRATION"`, `IOS_DIR=$(dirname
13
+ * "$IOS_PROJECT_PATH")` and `for WORKSPACE in "$IOS_DIR"/*.xcworkspace`, and
14
+ * the run died on "3 required value(s) missing — INTEGRATION, IOS_DIR,
15
+ * WORKSPACE" while the two variables it genuinely reads were both declared and
16
+ * present. The iOS templates do the same thing with `IOS_DIR` alone.
17
+ *
18
+ * The load-bearing half of these tests is the second half: only the assignment
19
+ * *target* is excused, never the right-hand side, so a script cannot exempt
20
+ * itself from a value it reads by mentioning it.
21
+ */
22
+ import '../yaml/steps/index.js';
23
+ //# sourceMappingURL=step-validator-self-assigned.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-validator-self-assigned.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator-self-assigned.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,wBAAwB,CAAC"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * A step does not owe itself the variables its own shell sets.
3
+ *
4
+ * Pre-execution validation auto-discovers every `$UPPERCASE` in a step's
5
+ * inputs and treats it as a value the pipeline must supply. For a `script`
6
+ * step the inputs *are* the shell body, so a variable the script computes was
7
+ * indistinguishable from one it needed handed to it — and because the check
8
+ * cannot prompt in a sandbox, the pipeline was refused before step 1 for
9
+ * values nothing was ever meant to provide.
10
+ *
11
+ * A generated multiplatform pipeline hit exactly that. Its prepare step opens
12
+ * with `INTEGRATION="$KMM_IOS_INTEGRATION"`, `IOS_DIR=$(dirname
13
+ * "$IOS_PROJECT_PATH")` and `for WORKSPACE in "$IOS_DIR"/*.xcworkspace`, and
14
+ * the run died on "3 required value(s) missing — INTEGRATION, IOS_DIR,
15
+ * WORKSPACE" while the two variables it genuinely reads were both declared and
16
+ * present. The iOS templates do the same thing with `IOS_DIR` alone.
17
+ *
18
+ * The load-bearing half of these tests is the second half: only the assignment
19
+ * *target* is excused, never the right-hand side, so a script cannot exempt
20
+ * itself from a value it reads by mentioning it.
21
+ */
22
+ import { describe, test, expect } from '@jest/globals';
23
+ import '../yaml/steps/index.js';
24
+ import { StepValidator } from './step-validator.js';
25
+ import { EnvResolver } from './env-resolver.js';
26
+ import { shellAssignedNames } from './shell-assignments.js';
27
+ import { loadConfig } from '../config.js';
28
+ const config = loadConfig();
29
+ /** A one-script workflow, so the only issues are about its variables. */
30
+ function scriptPipeline(content, envs = {}) {
31
+ return {
32
+ format_version: '1',
33
+ meta: { invarn: { stack: 'macos-xcode-26.4' } },
34
+ app: { envs: Object.entries(envs).map(([k, v]) => ({ [k]: v })) },
35
+ workflows: {
36
+ primary: {
37
+ steps: [{ 'script@1.0.0': { title: 'Prepare', inputs: { content } } }],
38
+ },
39
+ },
40
+ };
41
+ }
42
+ async function missingEnv(pipeline) {
43
+ const validator = new StepValidator(pipeline, 'primary', config);
44
+ const result = await validator.validateWorkflow();
45
+ return (result.issuesByCategory.get('environment') ?? [])
46
+ .map((i) => i.requirement.name)
47
+ .sort();
48
+ }
49
+ describe('shellAssignedNames', () => {
50
+ test('plain assignment, command substitution and quoted value', () => {
51
+ const found = shellAssignedNames({
52
+ content: [
53
+ 'PLAIN=1',
54
+ 'CAPTURED=$(dirname "$SOMEWHERE")',
55
+ 'QUOTED="a b"',
56
+ 'EMPTY=',
57
+ ].join('\n'),
58
+ });
59
+ expect([...found].sort()).toEqual(['CAPTURED', 'EMPTY', 'PLAIN', 'QUOTED']);
60
+ });
61
+ test('export, local, readonly, declare, typeset and append', () => {
62
+ const found = shellAssignedNames({
63
+ content: [
64
+ 'export EXPORTED=1',
65
+ ' local SCOPED=2',
66
+ 'readonly FROZEN=3',
67
+ 'declare DECLARED=4',
68
+ 'typeset TYPESET=5',
69
+ 'APPENDED+=6',
70
+ ].join('\n'),
71
+ });
72
+ expect([...found].sort()).toEqual([
73
+ 'APPENDED',
74
+ 'DECLARED',
75
+ 'EXPORTED',
76
+ 'FROZEN',
77
+ 'SCOPED',
78
+ 'TYPESET',
79
+ ]);
80
+ });
81
+ test('loop and read bindings', () => {
82
+ const found = shellAssignedNames({
83
+ content: [
84
+ 'for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do echo "$WORKSPACE"; done',
85
+ 'select CHOICE in a b; do break; done',
86
+ 'read -r LINE',
87
+ 'echo x | while read -r KEY VALUE; do echo "$KEY"; done',
88
+ ].join('\n'),
89
+ });
90
+ expect([...found].sort()).toEqual(['CHOICE', 'KEY', 'LINE', 'VALUE', 'WORKSPACE']);
91
+ });
92
+ test('a flag that looks like an assignment is not one', () => {
93
+ const found = shellAssignedNames({
94
+ content: 'xcodebuild --setting FOO=1 -destination PLATFORM=iOS\ngradlew -PSOME=2',
95
+ });
96
+ expect([...found]).toEqual([]);
97
+ });
98
+ test('walks nested inputs, not just a top-level string', () => {
99
+ const found = shellAssignedNames({ inputs: { content: 'NESTED=1' }, other: ['ALSO=2'] });
100
+ expect([...found].sort()).toEqual(['ALSO', 'NESTED']);
101
+ });
102
+ });
103
+ describe('pre-execution validation and self-assigned variables', () => {
104
+ // The shape that was refused, reduced to its first three lines.
105
+ const PREPARE = [
106
+ 'INTEGRATION="$KMM_IOS_INTEGRATION"',
107
+ 'IOS_DIR=$(dirname "$IOS_PROJECT_PATH")',
108
+ 'for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do echo "$WORKSPACE"; done',
109
+ ].join('\n');
110
+ test('asks for nothing when the script sets its own and the rest are declared', async () => {
111
+ expect(await missingEnv(scriptPipeline(PREPARE, {
112
+ KMM_IOS_INTEGRATION: 'direct',
113
+ IOS_PROJECT_PATH: 'iosApp/iosApp.xcodeproj',
114
+ }))).toEqual([]);
115
+ });
116
+ // The regression this relaxation could introduce, and the reason only the
117
+ // assignment target is excused: a script that READS a variable still needs
118
+ // it, even where it assigns something else from it on the same line.
119
+ test('still requires the right-hand side of a self-assignment', async () => {
120
+ expect(await missingEnv(scriptPipeline(PREPARE))).toEqual([
121
+ 'IOS_PROJECT_PATH',
122
+ 'KMM_IOS_INTEGRATION',
123
+ ]);
124
+ });
125
+ test('still requires a variable the script only reads', async () => {
126
+ expect(await missingEnv(scriptPipeline('SET_HERE=1\necho "$SET_HERE $NEVER_SET"'))).toEqual([
127
+ 'NEVER_SET',
128
+ ]);
129
+ });
130
+ });
131
+ /**
132
+ * The resolver half, and the worse half.
133
+ *
134
+ * `StepValidator` only *demands* a script's own locals. `EnvResolver`
135
+ * SUBSTITUTES them — so a defined `IOS_DIR` would paste a pipeline value over
136
+ * the one `dirname` had just computed, and an undefined one refuses the build.
137
+ * Both are wrong; leaving the reference for the shell is the only correct
138
+ * answer, and it is the same thing `registerStepOutput` already does for a
139
+ * value a previous step exports.
140
+ */
141
+ describe('EnvResolver leaves a script its own locals', () => {
142
+ const PREPARE = [
143
+ 'INTEGRATION="$KMM_IOS_INTEGRATION"',
144
+ 'IOS_DIR=$(dirname "$IOS_PROJECT_PATH")',
145
+ 'echo "$INTEGRATION in $IOS_DIR"',
146
+ 'for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do echo "$WORKSPACE"; done',
147
+ ].join('\n');
148
+ function resolver(envs) {
149
+ const pipeline = {
150
+ format_version: '1',
151
+ app: { envs: Object.entries(envs).map(([k, v]) => ({ [k]: v })) },
152
+ workflows: { primary: { steps: [] } },
153
+ };
154
+ return new EnvResolver(pipeline, pipeline.workflows.primary, 'primary', 'macos');
155
+ }
156
+ test('does not substitute or demand what the snippet assigns', () => {
157
+ const out = resolver({
158
+ KMM_IOS_INTEGRATION: 'cocoapods',
159
+ IOS_PROJECT_PATH: 'iosApp/iosApp.xcodeproj',
160
+ }).interpolate(PREPARE, 'Prepare the iOS build');
161
+ // The right-hand sides ARE resolved…
162
+ expect(out).toContain('INTEGRATION="cocoapods"');
163
+ expect(out).toContain('IOS_DIR=$(dirname "iosApp/iosApp.xcodeproj")');
164
+ // …and the locals are left for the shell, verbatim.
165
+ expect(out).toContain('echo "$INTEGRATION in $IOS_DIR"');
166
+ expect(out).toContain('for WORKSPACE in "$IOS_DIR"/*.xcworkspace');
167
+ expect(out).toContain('echo "$WORKSPACE"');
168
+ });
169
+ // Before the fix this threw MissingEnvironmentVariableError for INTEGRATION,
170
+ // which is what stopped every generated KMM and iOS pipeline before step 1.
171
+ test('does not throw for a local the snippet assigns', () => {
172
+ expect(() => resolver({
173
+ KMM_IOS_INTEGRATION: 'direct',
174
+ IOS_PROJECT_PATH: 'iosApp/iosApp.xcodeproj',
175
+ }).interpolate(PREPARE, 'Prepare the iOS build')).not.toThrow();
176
+ });
177
+ // The regression guard: a value the script only READS is still required.
178
+ test('still throws for a variable the snippet only reads', () => {
179
+ expect(() => resolver({}).interpolate(PREPARE, 'Prepare the iOS build')).toThrow(/Missing environment variable: 'KMM_IOS_INTEGRATION'/);
180
+ });
181
+ test('substitutes normally where the snippet assigns nothing', () => {
182
+ expect(resolver({ IOS_SCHEME: 'Hush iOS' }).interpolate('xcodebuild -scheme "$IOS_SCHEME"'))
183
+ .toBe('xcodebuild -scheme "Hush iOS"');
184
+ });
185
+ // `{{getenv "X"}}` is an explicit request by name, not a bare shell
186
+ // expansion, so it is honoured even where the snippet also assigns X.
187
+ test('still honours an explicit {{getenv}} for an assigned name', () => {
188
+ expect(resolver({ IOS_DIR: 'from-pipeline' }).interpolate('IOS_DIR=x\necho {{getenv "IOS_DIR"}}')).toContain('echo from-pipeline');
189
+ });
190
+ });
191
+ //# sourceMappingURL=step-validator-self-assigned.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"step-validator.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,uBAAuB,CAAC;AAwB/B;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAC,CAAS;IAG9B,OAAO,CAAC,gBAAgB,CAAoE;IAG5F,OAAO,CAAC,mBAAmB,CAA6B;IAKxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;gBAGvC,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM;IA0BvB;;;OAGG;IACG,gBAAgB,IAAI,OAAO,CAAC,wBAAwB,CAAC;IAkN3D;;OAEG;YACW,mBAAmB;IAmEjC;;;;;;;;;OASG;IACH,OAAO,CAAC,yBAAyB;IAsBjC;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAoB3B;;OAEG;IACH,OAAO,CAAC,SAAS;CA8BlB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CA4D/E"}
1
+ {"version":3,"file":"step-validator.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,uBAAuB,CAAC;AAwB/B;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAC,CAAS;IAG9B,OAAO,CAAC,gBAAgB,CAAoE;IAG5F,OAAO,CAAC,mBAAmB,CAA6B;IAKxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;gBAGvC,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM;IA0BvB;;;OAGG;IACG,gBAAgB,IAAI,OAAO,CAAC,wBAAwB,CAAC;IAwN3D;;OAEG;YACW,mBAAmB;IAmEjC;;;;;;;;;OASG;IACH,OAAO,CAAC,yBAAyB;IAsBjC;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAoB3B;;OAEG;IACH,OAAO,CAAC,SAAS;CA8BlB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CA4D/E"}
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { getStepExecutor, hasStep, getStepMetadata } from './steps/registry.js';
9
9
  import { EnvResolver } from './env-resolver.js';
10
+ import { shellAssignedNames } from './shell-assignments.js';
10
11
  import { detectPlatformInfo } from './platform-detector.js';
11
12
  import { declaresMultiplatformApp } from './meta-helpers.js';
12
13
  // POSIX env vars the shell always provides — don't flag these as undeclared.
@@ -169,10 +170,17 @@ export class StepValidator {
169
170
  const resolvedEnv = this.envResolver.getAll();
170
171
  const alreadyDeclared = new Set(requirements.map((r) => r.name));
171
172
  const referencedVars = this.extractVariableReferences(parsedStep.inputs);
173
+ // Variables the step's own shell sets are not values anyone owes it.
174
+ const selfAssigned = shellAssignedNames(parsedStep.inputs);
172
175
  for (const varName of referencedVars) {
173
176
  // Skip POSIX vars the shell always provides
174
177
  if (POSIX_ENV_ALLOWLIST.has(varName))
175
178
  continue;
179
+ // Skip anything this step's script assigns itself — a computed local
180
+ // reads like a required input, and refusing on one stops a pipeline
181
+ // for a value nothing was ever meant to supply.
182
+ if (selfAssigned.has(varName))
183
+ continue;
176
184
  // Skip if already in resolved env (use hasOwnProperty so empty-string
177
185
  // values set by the YAML/secrets are still treated as "provided")
178
186
  if (Object.prototype.hasOwnProperty.call(resolvedEnv, varName))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.6.8",
3
+ "version": "2.7.0",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",