apify-test-tools 0.10.2-beta.16 → 0.10.2-beta.18
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/.gitattributes +1 -0
- package/.github/cli/apify-test-tools.mjs +25 -25
- package/.github/scripts/run-with-apify-tokens.mjs +2 -128
- package/.github/workflows/_check_code.yaml +5 -5
- package/.husky/pre-commit +4 -4
- package/.prettierignore +1 -0
- package/CHANGELOG.md +1 -0
- package/bin/actor-ids.ts +11 -8
- package/bin/run-tests.ts +2 -1
- package/dist/bin/actor-ids.d.ts +5 -6
- package/dist/bin/actor-ids.d.ts.map +1 -1
- package/dist/bin/actor-ids.js +9 -3
- package/dist/bin/actor-ids.js.map +1 -1
- package/dist/bin/run-tests.d.ts.map +1 -1
- package/dist/bin/run-tests.js +1 -1
- package/dist/bin/run-tests.js.map +1 -1
- package/dist/test/unit/bin/actor-ids.test.js +11 -3
- package/dist/test/unit/bin/actor-ids.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js +124 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
- package/docs/cli.md +4 -3
- package/eslint.config.mjs +9 -0
- package/package.json +4 -2
- package/test/unit/bin/actor-ids.test.ts +11 -7
- package/tsconfig.json +1 -1
- package/workflow-scripts-src/run-with-apify-tokens.ts +141 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-with-apify-tokens.d.ts","sourceRoot":"","sources":["../../workflow-scripts-src/run-with-apify-tokens.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs apify-test-tools and passes it only secrets needed for the Actors, no other secrets are leaked.
|
|
3
|
+
//
|
|
4
|
+
// jq ... | node .github/scripts/run-with-apify-tokens.mjs apify-test-tools build --version-number 0.99
|
|
5
|
+
//
|
|
6
|
+
// The step passes the reusable workflow's whole secrets map as WORKFLOW_SECRETS. The token names
|
|
7
|
+
// come from `tokenEnvVar` in the repo's apify-test-tools.config.json, which is the same file
|
|
8
|
+
// apify-test-tools itself reads, so the two can't drift. Everything else in the secrets map (npm,
|
|
9
|
+
// Slack, GitHub, anything else the repo holds) is left out, and WORKFLOW_SECRETS itself is dropped,
|
|
10
|
+
// so the blob never reaches the CLI or anything under node_modules.
|
|
11
|
+
//
|
|
12
|
+
// Each token is looked up in the CONFIG_SECRETS secret first, a JSON object of secret name -> value,
|
|
13
|
+
// and then among the top-level secrets, which is where `secrets: inherit` puts them. The secrets the
|
|
14
|
+
// workflows read by name (NPM_TOKEN, TESTER_APIFY_TOKEN, ...) never come from CONFIG_SECRETS.
|
|
15
|
+
//
|
|
16
|
+
// The command runs without a shell, so branch names and other interpolated arguments are passed
|
|
17
|
+
// through as literal argv entries.
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
// Resolved against the working directory, matching how apify-test-tools' own readConfigFile
|
|
22
|
+
// locates it.
|
|
23
|
+
const CONFIG_FILE_NAME = 'apify-test-tools.config.json';
|
|
24
|
+
function fail(message) {
|
|
25
|
+
console.error(message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
const [command, ...args] = process.argv.slice(2);
|
|
32
|
+
if (!command)
|
|
33
|
+
fail('Usage: run-with-apify-tokens.mjs <command> [args...]');
|
|
34
|
+
const { WORKFLOW_SECRETS, ...baseEnv } = process.env;
|
|
35
|
+
if (!WORKFLOW_SECRETS) {
|
|
36
|
+
fail('WORKFLOW_SECRETS is not set. Add `WORKFLOW_SECRETS: ${{ toJSON(secrets) }}` to the step env.');
|
|
37
|
+
}
|
|
38
|
+
let workflowSecrets;
|
|
39
|
+
try {
|
|
40
|
+
workflowSecrets = JSON.parse(WORKFLOW_SECRETS);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
fail(`WORKFLOW_SECRETS is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
const workflowSecretsMap = workflowSecrets;
|
|
46
|
+
if (!isRecord(workflowSecretsMap)) {
|
|
47
|
+
fail('WORKFLOW_SECRETS must be a JSON object of secret names and values.');
|
|
48
|
+
}
|
|
49
|
+
// Never includes the parse error: Node quotes the input in it, and the input is a secret.
|
|
50
|
+
const unpackConfigSecrets = () => {
|
|
51
|
+
const packed = workflowSecretsMap.CONFIG_SECRETS;
|
|
52
|
+
if (!packed)
|
|
53
|
+
return {};
|
|
54
|
+
if (typeof packed !== 'string')
|
|
55
|
+
fail('The CONFIG_SECRETS secret is not valid JSON.');
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(packed);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
fail('The CONFIG_SECRETS secret is not valid JSON.');
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(value)) {
|
|
64
|
+
fail('The CONFIG_SECRETS secret must be a JSON object.');
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
const secrets = { ...workflowSecretsMap, ...unpackConfigSecrets() };
|
|
69
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE_NAME);
|
|
70
|
+
let config;
|
|
71
|
+
try {
|
|
72
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
fail(`Cannot read "${configPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
if (!isRecord(config) || !Array.isArray(config.actors)) {
|
|
78
|
+
fail(`"${configPath}" must have an "actors" array at the top level.`);
|
|
79
|
+
}
|
|
80
|
+
const tokenNames = [];
|
|
81
|
+
const entriesWithoutToken = [];
|
|
82
|
+
const actors = config.actors;
|
|
83
|
+
for (const [index, actor] of actors.entries()) {
|
|
84
|
+
const tokenEnvVar = isRecord(actor) ? actor.tokenEnvVar : undefined;
|
|
85
|
+
if (typeof tokenEnvVar !== 'string' || tokenEnvVar === '') {
|
|
86
|
+
entriesWithoutToken.push((isRecord(actor) ? actor.actorFullName : undefined) ?? `entry at index ${index}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!tokenNames.includes(tokenEnvVar))
|
|
90
|
+
tokenNames.push(tokenEnvVar);
|
|
91
|
+
}
|
|
92
|
+
tokenNames.sort();
|
|
93
|
+
if (entriesWithoutToken.length > 0) {
|
|
94
|
+
console.error(`Warning: no "tokenEnvVar" in ${CONFIG_FILE_NAME} for: ${entriesWithoutToken.join(', ')}. ` +
|
|
95
|
+
`Building those Actors will fail.`);
|
|
96
|
+
}
|
|
97
|
+
// Declared but absent is a warning, not an error. A repo can carry an Actor whose token isn't
|
|
98
|
+
// configured and still build fine as long as that Actor never changes, and apify-test-tools raises
|
|
99
|
+
// a precise error naming the Actor at the point it actually needs the token.
|
|
100
|
+
const missing = tokenNames.filter((name) => !(name in secrets));
|
|
101
|
+
if (missing.length > 0) {
|
|
102
|
+
console.error(`Warning: ${CONFIG_FILE_NAME} declares ${missing.join(', ')}, but no such secret was passed ` +
|
|
103
|
+
`to this workflow, neither inside CONFIG_SECRETS nor inherited. Check the repository secrets ` +
|
|
104
|
+
`and the workflow's \`secrets:\` if a build fails on a missing token.`);
|
|
105
|
+
}
|
|
106
|
+
const present = tokenNames.filter((name) => name in secrets);
|
|
107
|
+
// Names only. The values are secrets and must never be printed, even though the runner masks
|
|
108
|
+
// registered secrets in logs.
|
|
109
|
+
console.error(`Passing ${present.length} Actor token(s) to \`${command}\`: ${present.join(', ') || '(none)'}`);
|
|
110
|
+
const env = { ...baseEnv };
|
|
111
|
+
for (const name of present) {
|
|
112
|
+
const value = secrets[name];
|
|
113
|
+
if (typeof value !== 'string')
|
|
114
|
+
fail(`Secret "${name}" must be a string.`);
|
|
115
|
+
env[name] = value;
|
|
116
|
+
}
|
|
117
|
+
const result = spawnSync(command, args, { stdio: 'inherit', env });
|
|
118
|
+
if (result.error) {
|
|
119
|
+
fail(`Failed to run \`${command}\`: ${result.error.message}`);
|
|
120
|
+
}
|
|
121
|
+
// Preserve the child's exit code so the step fails exactly when the command does. A child killed
|
|
122
|
+
// by a signal reports a null status, which would otherwise be read as success.
|
|
123
|
+
process.exit(result.status ?? 1);
|
|
124
|
+
//# sourceMappingURL=run-with-apify-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-with-apify-tokens.js","sourceRoot":"","sources":["../../workflow-scripts-src/run-with-apify-tokens.ts"],"names":[],"mappings":"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"}
|
package/docs/cli.md
CHANGED
|
@@ -296,9 +296,10 @@ The input is a JSON array of Actors, each given by its full name, its raw ID or
|
|
|
296
296
|
with it (`buildId` and `buildNumber`, as `build` and `build-from-local` print them), or against its default build
|
|
297
297
|
without one. Other fields are ignored, so the output of [`get-actor-configs`](#listing-actors-get-actor-configs) works
|
|
298
298
|
as it is. `test` looks up the ID an Actor is listed without with `TESTER_APIFY_TOKEN`, so its tests run whichever ID
|
|
299
|
-
they name it by.
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
they name it by. An Actor the token can't see keeps only the ID it was listed with, and a warning names it, so an
|
|
300
|
+
Actor no test uses doesn't stop the run. A dry-run plan, empty or invalid input is an error, as with `build`. So is a
|
|
301
|
+
missing `TESTER_APIFY_TOKEN`: without it the lib skips every platform test, and the run would pass having tested
|
|
302
|
+
nothing. `test` passes the Actors to vitest as `ACTOR_BUILDS`, in place of any
|
|
302
303
|
`ACTOR_BUILDS` in its own environment.
|
|
303
304
|
|
|
304
305
|
It runs vitest with `--maxConcurrency=20 --fileParallelism --maxWorkers=100`, since platform tests mostly wait on
|
package/eslint.config.mjs
CHANGED
|
@@ -37,4 +37,13 @@ export default [
|
|
|
37
37
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
files: ['workflow-scripts-src/**/*.ts'],
|
|
42
|
+
rules: {
|
|
43
|
+
// Workflow helpers preserve the existing standalone scripts' console output and GitHub expression text.
|
|
44
|
+
'no-console': 'off',
|
|
45
|
+
'no-template-curly-in-string': 'off',
|
|
46
|
+
'prefer-destructuring': 'off',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
40
49
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apify-test-tools",
|
|
3
|
-
"version": "0.10.2-beta.
|
|
3
|
+
"version": "0.10.2-beta.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TBD",
|
|
6
6
|
"repository": {
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
"start": "npm run dist/bin/main.js",
|
|
60
60
|
"start:dev": "cd local-clone && tsx ../bin/main.ts",
|
|
61
61
|
"build": "tsc && chmod +x ./dist/bin/main.js",
|
|
62
|
-
"bundle": "
|
|
62
|
+
"bundle": "npm run bundle:cli && npm run bundle:workflow-scripts",
|
|
63
|
+
"bundle:cli": "esbuild bin/main.ts --bundle --minify --platform=node --format=esm --target=node24 --log-level=warning --banner:js=\"import{createRequire as __createRequire}from'node:module';const require=__createRequire(import.meta.url);\" --outfile=.github/cli/apify-test-tools.mjs",
|
|
64
|
+
"bundle:workflow-scripts": "esbuild workflow-scripts-src/run-with-apify-tokens.ts --bundle --minify --platform=node --format=esm --target=node24 --log-level=warning --banner:js=\"import{createRequire as __createRequire}from'node:module';const require=__createRequire(import.meta.url);\" --outfile=.github/scripts/run-with-apify-tokens.mjs",
|
|
63
65
|
"lint": "eslint",
|
|
64
66
|
"lint:fix": "eslint --fix",
|
|
65
67
|
"format": "prettier --write .",
|
|
@@ -32,12 +32,16 @@ describe('withActorIds', () => {
|
|
|
32
32
|
expect(client.actor).not.toHaveBeenCalled();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it('
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
35
|
+
it('keeps an Actor the token cannot see as listed, instead of failing the run', async () => {
|
|
36
|
+
const targets = [
|
|
37
|
+
{ actorFullName: 'owner/missing' },
|
|
38
|
+
{ actorRawId: 'rawMissing' },
|
|
39
|
+
{ actorFullName: 'owner/a' },
|
|
40
|
+
];
|
|
41
|
+
expect(await withActorIds(targets, fakeClient())).toEqual([
|
|
42
|
+
{ actorFullName: 'owner/missing' },
|
|
43
|
+
{ actorRawId: 'rawMissing' },
|
|
44
|
+
{ actorFullName: 'owner/a', actorRawId: 'rawA' },
|
|
45
|
+
]);
|
|
42
46
|
});
|
|
43
47
|
});
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs apify-test-tools and passes it only secrets needed for the Actors, no other secrets are leaked.
|
|
3
|
+
//
|
|
4
|
+
// jq ... | node .github/scripts/run-with-apify-tokens.mjs apify-test-tools build --version-number 0.99
|
|
5
|
+
//
|
|
6
|
+
// The step passes the reusable workflow's whole secrets map as WORKFLOW_SECRETS. The token names
|
|
7
|
+
// come from `tokenEnvVar` in the repo's apify-test-tools.config.json, which is the same file
|
|
8
|
+
// apify-test-tools itself reads, so the two can't drift. Everything else in the secrets map (npm,
|
|
9
|
+
// Slack, GitHub, anything else the repo holds) is left out, and WORKFLOW_SECRETS itself is dropped,
|
|
10
|
+
// so the blob never reaches the CLI or anything under node_modules.
|
|
11
|
+
//
|
|
12
|
+
// Each token is looked up in the CONFIG_SECRETS secret first, a JSON object of secret name -> value,
|
|
13
|
+
// and then among the top-level secrets, which is where `secrets: inherit` puts them. The secrets the
|
|
14
|
+
// workflows read by name (NPM_TOKEN, TESTER_APIFY_TOKEN, ...) never come from CONFIG_SECRETS.
|
|
15
|
+
//
|
|
16
|
+
// The command runs without a shell, so branch names and other interpolated arguments are passed
|
|
17
|
+
// through as literal argv entries.
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
// Resolved against the working directory, matching how apify-test-tools' own readConfigFile
|
|
24
|
+
// locates it.
|
|
25
|
+
const CONFIG_FILE_NAME = 'apify-test-tools.config.json';
|
|
26
|
+
|
|
27
|
+
function fail(message: string): never {
|
|
28
|
+
console.error(message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [command, ...args] = process.argv.slice(2);
|
|
37
|
+
if (!command) fail('Usage: run-with-apify-tokens.mjs <command> [args...]');
|
|
38
|
+
|
|
39
|
+
const { WORKFLOW_SECRETS, ...baseEnv } = process.env;
|
|
40
|
+
if (!WORKFLOW_SECRETS) {
|
|
41
|
+
fail('WORKFLOW_SECRETS is not set. Add `WORKFLOW_SECRETS: ${{ toJSON(secrets) }}` to the step env.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let workflowSecrets: unknown;
|
|
45
|
+
try {
|
|
46
|
+
workflowSecrets = JSON.parse(WORKFLOW_SECRETS);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail(`WORKFLOW_SECRETS is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
const workflowSecretsMap = workflowSecrets;
|
|
51
|
+
if (!isRecord(workflowSecretsMap)) {
|
|
52
|
+
fail('WORKFLOW_SECRETS must be a JSON object of secret names and values.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Never includes the parse error: Node quotes the input in it, and the input is a secret.
|
|
56
|
+
const unpackConfigSecrets = (): Record<string, unknown> => {
|
|
57
|
+
const packed = workflowSecretsMap.CONFIG_SECRETS;
|
|
58
|
+
if (!packed) return {};
|
|
59
|
+
if (typeof packed !== 'string') fail('The CONFIG_SECRETS secret is not valid JSON.');
|
|
60
|
+
let value: unknown;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(packed);
|
|
63
|
+
} catch {
|
|
64
|
+
fail('The CONFIG_SECRETS secret is not valid JSON.');
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(value)) {
|
|
67
|
+
fail('The CONFIG_SECRETS secret must be a JSON object.');
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const secrets = { ...workflowSecretsMap, ...unpackConfigSecrets() };
|
|
73
|
+
|
|
74
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE_NAME);
|
|
75
|
+
let config: unknown;
|
|
76
|
+
try {
|
|
77
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
fail(`Cannot read "${configPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isRecord(config) || !Array.isArray(config.actors)) {
|
|
83
|
+
fail(`"${configPath}" must have an "actors" array at the top level.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tokenNames: string[] = [];
|
|
87
|
+
const entriesWithoutToken: unknown[] = [];
|
|
88
|
+
|
|
89
|
+
const actors: unknown[] = config.actors;
|
|
90
|
+
for (const [index, actor] of actors.entries()) {
|
|
91
|
+
const tokenEnvVar = isRecord(actor) ? actor.tokenEnvVar : undefined;
|
|
92
|
+
if (typeof tokenEnvVar !== 'string' || tokenEnvVar === '') {
|
|
93
|
+
entriesWithoutToken.push((isRecord(actor) ? actor.actorFullName : undefined) ?? `entry at index ${index}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!tokenNames.includes(tokenEnvVar)) tokenNames.push(tokenEnvVar);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tokenNames.sort();
|
|
100
|
+
|
|
101
|
+
if (entriesWithoutToken.length > 0) {
|
|
102
|
+
console.error(
|
|
103
|
+
`Warning: no "tokenEnvVar" in ${CONFIG_FILE_NAME} for: ${entriesWithoutToken.join(', ')}. ` +
|
|
104
|
+
`Building those Actors will fail.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Declared but absent is a warning, not an error. A repo can carry an Actor whose token isn't
|
|
109
|
+
// configured and still build fine as long as that Actor never changes, and apify-test-tools raises
|
|
110
|
+
// a precise error naming the Actor at the point it actually needs the token.
|
|
111
|
+
const missing = tokenNames.filter((name) => !(name in secrets));
|
|
112
|
+
if (missing.length > 0) {
|
|
113
|
+
console.error(
|
|
114
|
+
`Warning: ${CONFIG_FILE_NAME} declares ${missing.join(', ')}, but no such secret was passed ` +
|
|
115
|
+
`to this workflow, neither inside CONFIG_SECRETS nor inherited. Check the repository secrets ` +
|
|
116
|
+
`and the workflow's \`secrets:\` if a build fails on a missing token.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const present = tokenNames.filter((name) => name in secrets);
|
|
121
|
+
|
|
122
|
+
// Names only. The values are secrets and must never be printed, even though the runner masks
|
|
123
|
+
// registered secrets in logs.
|
|
124
|
+
console.error(`Passing ${present.length} Actor token(s) to \`${command}\`: ${present.join(', ') || '(none)'}`);
|
|
125
|
+
|
|
126
|
+
const env = { ...baseEnv };
|
|
127
|
+
for (const name of present) {
|
|
128
|
+
const value = secrets[name];
|
|
129
|
+
if (typeof value !== 'string') fail(`Secret "${name}" must be a string.`);
|
|
130
|
+
env[name] = value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = spawnSync(command, args, { stdio: 'inherit', env });
|
|
134
|
+
|
|
135
|
+
if (result.error) {
|
|
136
|
+
fail(`Failed to run \`${command}\`: ${result.error.message}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Preserve the child's exit code so the step fails exactly when the command does. A child killed
|
|
140
|
+
// by a signal reports a null status, which would otherwise be read as success.
|
|
141
|
+
process.exit(result.status ?? 1);
|