@invarn/cibuild 2.6.8 → 2.6.9
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 +9 -9
- package/dist/src/yaml/step-validator-self-assigned.test.d.ts +23 -0
- package/dist/src/yaml/step-validator-self-assigned.test.d.ts.map +1 -0
- package/dist/src/yaml/step-validator-self-assigned.test.js +129 -0
- package/dist/src/yaml/step-validator.d.ts +32 -0
- package/dist/src/yaml/step-validator.d.ts.map +1 -1
- package/dist/src/yaml/step-validator.js +72 -0
- package/package.json +1 -1
|
@@ -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,129 @@
|
|
|
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, shellAssignedNames } from './step-validator.js';
|
|
25
|
+
import { loadConfig } from '../config.js';
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
/** A one-script workflow, so the only issues are about its variables. */
|
|
28
|
+
function scriptPipeline(content, envs = {}) {
|
|
29
|
+
return {
|
|
30
|
+
format_version: '1',
|
|
31
|
+
meta: { invarn: { stack: 'macos-xcode-26.4' } },
|
|
32
|
+
app: { envs: Object.entries(envs).map(([k, v]) => ({ [k]: v })) },
|
|
33
|
+
workflows: {
|
|
34
|
+
primary: {
|
|
35
|
+
steps: [{ 'script@1.0.0': { title: 'Prepare', inputs: { content } } }],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function missingEnv(pipeline) {
|
|
41
|
+
const validator = new StepValidator(pipeline, 'primary', config);
|
|
42
|
+
const result = await validator.validateWorkflow();
|
|
43
|
+
return (result.issuesByCategory.get('environment') ?? [])
|
|
44
|
+
.map((i) => i.requirement.name)
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
describe('shellAssignedNames', () => {
|
|
48
|
+
test('plain assignment, command substitution and quoted value', () => {
|
|
49
|
+
const found = shellAssignedNames({
|
|
50
|
+
content: [
|
|
51
|
+
'PLAIN=1',
|
|
52
|
+
'CAPTURED=$(dirname "$SOMEWHERE")',
|
|
53
|
+
'QUOTED="a b"',
|
|
54
|
+
'EMPTY=',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
});
|
|
57
|
+
expect([...found].sort()).toEqual(['CAPTURED', 'EMPTY', 'PLAIN', 'QUOTED']);
|
|
58
|
+
});
|
|
59
|
+
test('export, local, readonly, declare, typeset and append', () => {
|
|
60
|
+
const found = shellAssignedNames({
|
|
61
|
+
content: [
|
|
62
|
+
'export EXPORTED=1',
|
|
63
|
+
' local SCOPED=2',
|
|
64
|
+
'readonly FROZEN=3',
|
|
65
|
+
'declare DECLARED=4',
|
|
66
|
+
'typeset TYPESET=5',
|
|
67
|
+
'APPENDED+=6',
|
|
68
|
+
].join('\n'),
|
|
69
|
+
});
|
|
70
|
+
expect([...found].sort()).toEqual([
|
|
71
|
+
'APPENDED',
|
|
72
|
+
'DECLARED',
|
|
73
|
+
'EXPORTED',
|
|
74
|
+
'FROZEN',
|
|
75
|
+
'SCOPED',
|
|
76
|
+
'TYPESET',
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
test('loop and read bindings', () => {
|
|
80
|
+
const found = shellAssignedNames({
|
|
81
|
+
content: [
|
|
82
|
+
'for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do echo "$WORKSPACE"; done',
|
|
83
|
+
'select CHOICE in a b; do break; done',
|
|
84
|
+
'read -r LINE',
|
|
85
|
+
'echo x | while read -r KEY VALUE; do echo "$KEY"; done',
|
|
86
|
+
].join('\n'),
|
|
87
|
+
});
|
|
88
|
+
expect([...found].sort()).toEqual(['CHOICE', 'KEY', 'LINE', 'VALUE', 'WORKSPACE']);
|
|
89
|
+
});
|
|
90
|
+
test('a flag that looks like an assignment is not one', () => {
|
|
91
|
+
const found = shellAssignedNames({
|
|
92
|
+
content: 'xcodebuild --setting FOO=1 -destination PLATFORM=iOS\ngradlew -PSOME=2',
|
|
93
|
+
});
|
|
94
|
+
expect([...found]).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
test('walks nested inputs, not just a top-level string', () => {
|
|
97
|
+
const found = shellAssignedNames({ inputs: { content: 'NESTED=1' }, other: ['ALSO=2'] });
|
|
98
|
+
expect([...found].sort()).toEqual(['ALSO', 'NESTED']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('pre-execution validation and self-assigned variables', () => {
|
|
102
|
+
// The shape that was refused, reduced to its first three lines.
|
|
103
|
+
const PREPARE = [
|
|
104
|
+
'INTEGRATION="$KMM_IOS_INTEGRATION"',
|
|
105
|
+
'IOS_DIR=$(dirname "$IOS_PROJECT_PATH")',
|
|
106
|
+
'for WORKSPACE in "$IOS_DIR"/*.xcworkspace; do echo "$WORKSPACE"; done',
|
|
107
|
+
].join('\n');
|
|
108
|
+
test('asks for nothing when the script sets its own and the rest are declared', async () => {
|
|
109
|
+
expect(await missingEnv(scriptPipeline(PREPARE, {
|
|
110
|
+
KMM_IOS_INTEGRATION: 'direct',
|
|
111
|
+
IOS_PROJECT_PATH: 'iosApp/iosApp.xcodeproj',
|
|
112
|
+
}))).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
// The regression this relaxation could introduce, and the reason only the
|
|
115
|
+
// assignment target is excused: a script that READS a variable still needs
|
|
116
|
+
// it, even where it assigns something else from it on the same line.
|
|
117
|
+
test('still requires the right-hand side of a self-assignment', async () => {
|
|
118
|
+
expect(await missingEnv(scriptPipeline(PREPARE))).toEqual([
|
|
119
|
+
'IOS_PROJECT_PATH',
|
|
120
|
+
'KMM_IOS_INTEGRATION',
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
test('still requires a variable the script only reads', async () => {
|
|
124
|
+
expect(await missingEnv(scriptPipeline('SET_HERE=1\necho "$SET_HERE $NEVER_SET"'))).toEqual([
|
|
125
|
+
'NEVER_SET',
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=step-validator-self-assigned.test.js.map
|
|
@@ -8,6 +8,38 @@
|
|
|
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>;
|
|
11
43
|
/**
|
|
12
44
|
* StepValidator orchestrates pre-execution validation for a workflow.
|
|
13
45
|
* 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;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;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CA6B5D;AAED;;;;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"}
|
|
@@ -30,6 +30,71 @@ const POSIX_ENV_ALLOWLIST = new Set([
|
|
|
30
30
|
'CIBUILD_SCRATCH',
|
|
31
31
|
'JAVA_HOME',
|
|
32
32
|
]);
|
|
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
|
+
}
|
|
33
98
|
/**
|
|
34
99
|
* StepValidator orchestrates pre-execution validation for a workflow.
|
|
35
100
|
* It iterates through all steps, collects validation requirements,
|
|
@@ -169,10 +234,17 @@ export class StepValidator {
|
|
|
169
234
|
const resolvedEnv = this.envResolver.getAll();
|
|
170
235
|
const alreadyDeclared = new Set(requirements.map((r) => r.name));
|
|
171
236
|
const referencedVars = this.extractVariableReferences(parsedStep.inputs);
|
|
237
|
+
// Variables the step's own shell sets are not values anyone owes it.
|
|
238
|
+
const selfAssigned = shellAssignedNames(parsedStep.inputs);
|
|
172
239
|
for (const varName of referencedVars) {
|
|
173
240
|
// Skip POSIX vars the shell always provides
|
|
174
241
|
if (POSIX_ENV_ALLOWLIST.has(varName))
|
|
175
242
|
continue;
|
|
243
|
+
// Skip anything this step's script assigns itself — a computed local
|
|
244
|
+
// reads like a required input, and refusing on one stops a pipeline
|
|
245
|
+
// for a value nothing was ever meant to supply.
|
|
246
|
+
if (selfAssigned.has(varName))
|
|
247
|
+
continue;
|
|
176
248
|
// Skip if already in resolved env (use hasOwnProperty so empty-string
|
|
177
249
|
// values set by the YAML/secrets are still treated as "provided")
|
|
178
250
|
if (Object.prototype.hasOwnProperty.call(resolvedEnv, varName))
|