@invarn/cibuild 2.6.9 → 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.
- package/dist/cli.cjs +10 -10
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +14 -3
- package/dist/src/yaml/shell-assignments.d.ts +40 -0
- package/dist/src/yaml/shell-assignments.d.ts.map +1 -0
- package/dist/src/yaml/shell-assignments.js +77 -0
- package/dist/src/yaml/step-validator-self-assigned.test.js +63 -1
- package/dist/src/yaml/step-validator.d.ts +0 -32
- package/dist/src/yaml/step-validator.d.ts.map +1 -1
- package/dist/src/yaml/step-validator.js +1 -65
- package/package.json +1 -1
|
@@ -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;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
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { describe, test, expect } from '@jest/globals';
|
|
23
23
|
import '../yaml/steps/index.js';
|
|
24
|
-
import { StepValidator
|
|
24
|
+
import { StepValidator } from './step-validator.js';
|
|
25
|
+
import { EnvResolver } from './env-resolver.js';
|
|
26
|
+
import { shellAssignedNames } from './shell-assignments.js';
|
|
25
27
|
import { loadConfig } from '../config.js';
|
|
26
28
|
const config = loadConfig();
|
|
27
29
|
/** A one-script workflow, so the only issues are about its variables. */
|
|
@@ -126,4 +128,64 @@ describe('pre-execution validation and self-assigned variables', () => {
|
|
|
126
128
|
]);
|
|
127
129
|
});
|
|
128
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
|
+
});
|
|
129
191
|
//# sourceMappingURL=step-validator-self-assigned.test.js.map
|
|
@@ -8,38 +8,6 @@
|
|
|
8
8
|
import type { YAMLPipeline } from './types.js';
|
|
9
9
|
import type { CIConfig } from '../types.js';
|
|
10
10
|
import type { WorkflowValidationResult } from './validation-types.js';
|
|
11
|
-
/**
|
|
12
|
-
* Names a shell snippet assigns to itself, in every form a step script uses.
|
|
13
|
-
*
|
|
14
|
-
* The auto-discovery below treats every `$UPPERCASE` it finds in a step's
|
|
15
|
-
* inputs as a value the pipeline must supply. For a `script` step the inputs
|
|
16
|
-
* *are* the shell body, so a variable the script computes reads exactly like
|
|
17
|
-
* one it needs handed to it — and the run is refused for a value nothing was
|
|
18
|
-
* ever supposed to provide.
|
|
19
|
-
*
|
|
20
|
-
* That is not hypothetical. A generated multiplatform pipeline whose prepare
|
|
21
|
-
* step opens with
|
|
22
|
-
*
|
|
23
|
-
* INTEGRATION="$KMM_IOS_INTEGRATION"
|
|
24
|
-
* IOS_DIR=$(dirname "$IOS_PROJECT_PATH")
|
|
25
|
-
* for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do …
|
|
26
|
-
*
|
|
27
|
-
* was refused before step 1 on "3 required value(s) missing — INTEGRATION,
|
|
28
|
-
* IOS_DIR, WORKSPACE", while the two variables it genuinely reads were both
|
|
29
|
-
* declared and present. The shell was correct; the reader was not.
|
|
30
|
-
*
|
|
31
|
-
* Only the assignment *target* is collected, never the right-hand side, so
|
|
32
|
-
* `INTEGRATION="$KMM_IOS_INTEGRATION"` still requires `KMM_IOS_INTEGRATION`.
|
|
33
|
-
* That is what keeps this from becoming a hole: a script cannot excuse itself
|
|
34
|
-
* from a value it reads by mentioning it.
|
|
35
|
-
*
|
|
36
|
-
* Assignment order is deliberately not modelled. A snippet that reads `$FOO`
|
|
37
|
-
* before setting it is a bug in the snippet, and guessing about it here would
|
|
38
|
-
* trade a false refusal for a different false refusal; "the script sets this
|
|
39
|
-
* somewhere" is the question worth asking, and erring toward letting the build
|
|
40
|
-
* run is the right direction for a check that cannot prompt.
|
|
41
|
-
*/
|
|
42
|
-
export declare function shellAssignedNames(obj: unknown): Set<string>;
|
|
43
11
|
/**
|
|
44
12
|
* StepValidator orchestrates pre-execution validation for a workflow.
|
|
45
13
|
* It iterates through all steps, collects validation requirements,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-validator.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
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.
|
|
@@ -30,71 +31,6 @@ const POSIX_ENV_ALLOWLIST = new Set([
|
|
|
30
31
|
'CIBUILD_SCRATCH',
|
|
31
32
|
'JAVA_HOME',
|
|
32
33
|
]);
|
|
33
|
-
/**
|
|
34
|
-
* Names a shell snippet assigns to itself, in every form a step script uses.
|
|
35
|
-
*
|
|
36
|
-
* The auto-discovery below treats every `$UPPERCASE` it finds in a step's
|
|
37
|
-
* inputs as a value the pipeline must supply. For a `script` step the inputs
|
|
38
|
-
* *are* the shell body, so a variable the script computes reads exactly like
|
|
39
|
-
* one it needs handed to it — and the run is refused for a value nothing was
|
|
40
|
-
* ever supposed to provide.
|
|
41
|
-
*
|
|
42
|
-
* That is not hypothetical. A generated multiplatform pipeline whose prepare
|
|
43
|
-
* step opens with
|
|
44
|
-
*
|
|
45
|
-
* INTEGRATION="$KMM_IOS_INTEGRATION"
|
|
46
|
-
* IOS_DIR=$(dirname "$IOS_PROJECT_PATH")
|
|
47
|
-
* for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do …
|
|
48
|
-
*
|
|
49
|
-
* was refused before step 1 on "3 required value(s) missing — INTEGRATION,
|
|
50
|
-
* IOS_DIR, WORKSPACE", while the two variables it genuinely reads were both
|
|
51
|
-
* declared and present. The shell was correct; the reader was not.
|
|
52
|
-
*
|
|
53
|
-
* Only the assignment *target* is collected, never the right-hand side, so
|
|
54
|
-
* `INTEGRATION="$KMM_IOS_INTEGRATION"` still requires `KMM_IOS_INTEGRATION`.
|
|
55
|
-
* That is what keeps this from becoming a hole: a script cannot excuse itself
|
|
56
|
-
* from a value it reads by mentioning it.
|
|
57
|
-
*
|
|
58
|
-
* Assignment order is deliberately not modelled. A snippet that reads `$FOO`
|
|
59
|
-
* before setting it is a bug in the snippet, and guessing about it here would
|
|
60
|
-
* trade a false refusal for a different false refusal; "the script sets this
|
|
61
|
-
* somewhere" is the question worth asking, and erring toward letting the build
|
|
62
|
-
* run is the right direction for a check that cannot prompt.
|
|
63
|
-
*/
|
|
64
|
-
export function shellAssignedNames(obj) {
|
|
65
|
-
const names = new Set();
|
|
66
|
-
const collect = (text) => {
|
|
67
|
-
// `NAME=…`, `export NAME=…`, `local/readonly/declare/typeset NAME=…`, and
|
|
68
|
-
// `NAME+=…`. Anchored to the start of a command — line start, or after a
|
|
69
|
-
// separator — so `--flag=X` and `foo BAR=1` inside a longer word are not
|
|
70
|
-
// mistaken for assignments.
|
|
71
|
-
const assignment = /(?:^|[\n;&|(]|\|\||&&)[ \t]*(?:(?:export|readonly|local|declare|typeset)[ \t]+)?([A-Z_][A-Z_0-9]*)\+?=/g;
|
|
72
|
-
// `for NAME in …`, `select NAME in …`
|
|
73
|
-
const loop = /(?:^|[\n;&|(])[ \t]*(?:for|select)[ \t]+([A-Z_][A-Z_0-9]*)[ \t]+in\b/g;
|
|
74
|
-
// `read NAME`, `read -r NAME OTHER`, `while read -r NAME; do`
|
|
75
|
-
const read = /(?:^|[\n;&|(]|\|\||&&)[ \t]*(?:while[ \t]+)?read[ \t]+((?:-\S+[ \t]+)*)([A-Z_][A-Z_0-9]*(?:[ \t]+[A-Z_][A-Z_0-9]*)*)/g;
|
|
76
|
-
for (const re of [assignment, loop]) {
|
|
77
|
-
let m;
|
|
78
|
-
while ((m = re.exec(text)) !== null)
|
|
79
|
-
names.add(m[1]);
|
|
80
|
-
}
|
|
81
|
-
let m;
|
|
82
|
-
while ((m = read.exec(text)) !== null) {
|
|
83
|
-
for (const n of m[2].trim().split(/[ \t]+/))
|
|
84
|
-
names.add(n);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const walk = (value) => {
|
|
88
|
-
if (typeof value === 'string')
|
|
89
|
-
collect(value);
|
|
90
|
-
else if (Array.isArray(value))
|
|
91
|
-
value.forEach(walk);
|
|
92
|
-
else if (value && typeof value === 'object')
|
|
93
|
-
Object.values(value).forEach(walk);
|
|
94
|
-
};
|
|
95
|
-
walk(obj);
|
|
96
|
-
return names;
|
|
97
|
-
}
|
|
98
34
|
/**
|
|
99
35
|
* StepValidator orchestrates pre-execution validation for a workflow.
|
|
100
36
|
* It iterates through all steps, collects validation requirements,
|