dd-trace 5.117.0 → 5.118.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/ci/diagnose.js +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +3 -26
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +75 -13
- package/ci/test-optimization-validation/generated-test-contract.js +283 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +7 -0
- package/package.json +2 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
- package/packages/dd-trace/src/llmobs/tagger.js +23 -3
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- package/ci/test-optimization-validation-manifest.schema.json +0 -1
|
@@ -1,480 +1,251 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
-
|
|
5
3
|
const crypto = require('node:crypto')
|
|
6
4
|
const fs = require('node:fs')
|
|
7
5
|
const path = require('node:path')
|
|
8
6
|
|
|
9
7
|
const { bindApprovedExecutable, getApprovedExecutable } = require('./executable-approval')
|
|
10
|
-
const {
|
|
8
|
+
const { getManifestCommands } = require('./runner-command')
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
* Returns
|
|
11
|
+
* Returns the unavailable file for a direct command.
|
|
14
12
|
*
|
|
15
|
-
* @param {object} command
|
|
13
|
+
* @param {object} command validator-owned command
|
|
16
14
|
* @returns {string|undefined} unavailable executable
|
|
17
15
|
*/
|
|
18
16
|
function getUnavailableExecutable (command) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
17
|
+
if (!isDirectRunnerCommand(command)) return command?.argv?.[0] || 'invalid direct-runner command'
|
|
18
|
+
for (const filename of [process.execPath, command.argv[1]]) {
|
|
19
|
+
if (!isRegularFile(filename)) return filename
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @param {object} command manifest command
|
|
29
|
-
* @returns {string|undefined} command executable
|
|
30
|
-
*/
|
|
31
|
-
function getExecutable (command) {
|
|
32
|
-
if (!command?.usesShell) return command?.argv?.[0]
|
|
33
|
-
if (typeof command.shell === 'string' && command.shell.trim()) return command.shell.trim()
|
|
34
|
-
return process.platform === 'win32'
|
|
35
|
-
? process.env.ComSpec || process.env.COMSPEC || 'cmd.exe'
|
|
36
|
-
: process.env.SHELL || '/bin/sh'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolves an executable from the command working directory or PATH.
|
|
24
|
+
* Returns the trusted Node.js executable used for every validation command.
|
|
41
25
|
*
|
|
42
|
-
* @param {
|
|
43
|
-
* @
|
|
44
|
-
* @returns {boolean} whether the executable can be resolved
|
|
45
|
-
*/
|
|
46
|
-
function resolveExecutable (executable, command) {
|
|
47
|
-
if (isExplicitExecutablePath(executable)) {
|
|
48
|
-
return isExecutable(path.resolve(command.cwd, executable))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const environmentPath = getEnvironmentPath(command)
|
|
52
|
-
const extensions = getExecutableExtensions()
|
|
53
|
-
|
|
54
|
-
for (const directory of environmentPath.split(path.delimiter)) {
|
|
55
|
-
if (!directory) continue
|
|
56
|
-
const resolvedDirectory = path.resolve(command.cwd, directory)
|
|
57
|
-
for (const extension of extensions) {
|
|
58
|
-
const filename = path.join(resolvedDirectory, `${executable}${extension}`)
|
|
59
|
-
if (isExecutable(filename)) return true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Resolves the filesystem path used for a structured command executable.
|
|
67
|
-
*
|
|
68
|
-
* @param {object} command manifest command
|
|
69
|
-
* @returns {string|undefined} resolved executable path
|
|
26
|
+
* @param {object} command validator-owned command
|
|
27
|
+
* @returns {string|undefined} Node.js executable
|
|
70
28
|
*/
|
|
71
29
|
function getResolvedExecutable (command) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (isExplicitExecutablePath(executable)) {
|
|
76
|
-
const filename = path.resolve(command.cwd, executable)
|
|
77
|
-
return isExecutable(filename) ? filename : undefined
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const environmentPath = getEnvironmentPath(command)
|
|
81
|
-
const extensions = getExecutableExtensions()
|
|
82
|
-
|
|
83
|
-
for (const directory of environmentPath.split(path.delimiter)) {
|
|
84
|
-
if (!directory) continue
|
|
85
|
-
const resolvedDirectory = path.resolve(command.cwd, directory)
|
|
86
|
-
for (const extension of extensions) {
|
|
87
|
-
const filename = path.join(resolvedDirectory, `${executable}${extension}`)
|
|
88
|
-
if (isExecutable(filename)) return filename
|
|
89
|
-
}
|
|
90
|
-
}
|
|
30
|
+
return isDirectRunnerCommand(command) && isRegularFile(process.execPath)
|
|
31
|
+
? fs.realpathSync(process.execPath)
|
|
32
|
+
: undefined
|
|
91
33
|
}
|
|
92
34
|
|
|
93
35
|
/**
|
|
94
|
-
*
|
|
36
|
+
* Fingerprints and binds every runnable framework's Node.js and runner files.
|
|
95
37
|
*
|
|
96
|
-
* @param {object}
|
|
97
|
-
* @returns {
|
|
98
|
-
*/
|
|
99
|
-
function getEnvironmentPath (command) {
|
|
100
|
-
if (Object.hasOwn(command.env || {}, 'PATH')) return command.env.PATH || ''
|
|
101
|
-
return process.env.PATH || ''
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Binds every executable selected by an approvable manifest command to its canonical file identity.
|
|
106
|
-
*
|
|
107
|
-
* @param {object} manifest loaded manifest
|
|
108
|
-
* @returns {object[]} sorted executable identities included in approval material
|
|
38
|
+
* @param {object} manifest normalized manifest
|
|
39
|
+
* @returns {object[]} executable identities
|
|
109
40
|
*/
|
|
110
41
|
function bindManifestExecutables (manifest) {
|
|
111
42
|
const identities = []
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
43
|
+
for (const framework of manifest.frameworks || []) {
|
|
44
|
+
if (framework.status !== 'runnable') continue
|
|
45
|
+
const command = getManifestCommands({ frameworks: [framework] })[0]?.[1]
|
|
46
|
+
try {
|
|
47
|
+
const identity = getCommandExecutableIdentity(command, manifest.repository.root)
|
|
48
|
+
bindApprovedExecutable(framework.validation, identity)
|
|
49
|
+
identities.push({ id: `framework:${framework.id}`, ...identity })
|
|
50
|
+
} catch (error) {
|
|
51
|
+
identities.push({
|
|
52
|
+
id: `framework:${framework.id}`,
|
|
53
|
+
unavailable: true,
|
|
54
|
+
reason: error?.message || String(error),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
119
57
|
}
|
|
120
|
-
return identities
|
|
58
|
+
return identities
|
|
121
59
|
}
|
|
122
60
|
|
|
123
61
|
/**
|
|
124
|
-
*
|
|
62
|
+
* Resolves a checksum-approved executable immediately before spawn.
|
|
125
63
|
*
|
|
126
|
-
* @param {object} command command
|
|
127
|
-
* @param {
|
|
128
|
-
* @
|
|
64
|
+
* @param {object} command validator-owned command
|
|
65
|
+
* @param {object} [options] resolution options
|
|
66
|
+
* @param {boolean} [options.requireApproval] whether approval is mandatory
|
|
67
|
+
* @returns {{argv0: string, path: string}} spawn executable
|
|
129
68
|
*/
|
|
130
69
|
function getExecutableForSpawn (command, options = {}) {
|
|
131
70
|
const approved = getApprovedExecutable(command)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (options.requireApproval) {
|
|
135
|
-
throw new Error('The selected command executable was not covered by the approved execution plan.')
|
|
136
|
-
}
|
|
137
|
-
if (!current) throw new Error(`Command executable is unavailable: ${getExecutable(command)}`)
|
|
138
|
-
return getLaunchIdentity(current)
|
|
71
|
+
if (options.requireApproval && !approved) {
|
|
72
|
+
throw new Error('Direct-runner command is not bound to the approved execution plan.')
|
|
139
73
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
74
|
+
const current = getCommandExecutableIdentity(command, approved?.repositoryRoot)
|
|
75
|
+
if (approved && !areExecutableIdentitiesEqual(current, approved)) {
|
|
76
|
+
throw new Error('The approved Node.js or test-runner executable changed after approval.')
|
|
144
77
|
}
|
|
145
|
-
return
|
|
78
|
+
return { argv0: process.execPath, path: current.path }
|
|
146
79
|
}
|
|
147
80
|
|
|
148
81
|
/**
|
|
149
|
-
*
|
|
82
|
+
* Builds the executable identity for one direct-runner command.
|
|
150
83
|
*
|
|
151
|
-
* @param {object} command
|
|
152
|
-
* @param {
|
|
153
|
-
* @returns {
|
|
84
|
+
* @param {object} command validator-owned command
|
|
85
|
+
* @param {string} repositoryRoot repository root
|
|
86
|
+
* @returns {object} executable identity
|
|
154
87
|
*/
|
|
155
|
-
function getCommandExecutableIdentity (command,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const identity = getExecutableIdentity(executableCommand, identitiesByPath)
|
|
159
|
-
if (!identity) return
|
|
160
|
-
identities.push(identity)
|
|
88
|
+
function getCommandExecutableIdentity (command, repositoryRoot) {
|
|
89
|
+
if (!isDirectRunnerCommand(command)) {
|
|
90
|
+
throw new Error('Only validator-owned `node <contained-runner> ...` commands may execute.')
|
|
161
91
|
}
|
|
162
92
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
*
|
|
170
|
-
* @param {object|undefined} current current identity tree
|
|
171
|
-
* @param {object|undefined} approved approved identity tree
|
|
172
|
-
* @returns {boolean} whether every executable matches
|
|
173
|
-
*/
|
|
174
|
-
function areExecutableIdentitiesEqual (current, approved) {
|
|
175
|
-
if (!current || !approved) return false
|
|
176
|
-
const currentIdentities = [current, ...(current.delegated || [])]
|
|
177
|
-
const approvedIdentities = [approved, ...(approved.delegated || [])]
|
|
178
|
-
if (currentIdentities.length !== approvedIdentities.length) return false
|
|
179
|
-
|
|
180
|
-
return currentIdentities.every((identity, index) => {
|
|
181
|
-
const expected = approvedIdentities[index]
|
|
182
|
-
return identity.invocationPath === expected.invocationPath && identity.path === expected.path &&
|
|
183
|
-
identity.sha256 === expected.sha256
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Expands nested env wrappers into the commands whose executables they delegate to.
|
|
189
|
-
*
|
|
190
|
-
* @param {object} command manifest command
|
|
191
|
-
* @returns {object[]} launcher followed by delegated commands
|
|
192
|
-
*/
|
|
193
|
-
function getExecutableCommands (command) {
|
|
194
|
-
const commands = [command]
|
|
195
|
-
let current = command
|
|
196
|
-
while (!current.usesShell && isEnvExecutable(current.argv?.[0])) {
|
|
197
|
-
current = getEnvDelegatedCommand(current)
|
|
198
|
-
commands.push(current)
|
|
93
|
+
repositoryRoot ||= command.cwd
|
|
94
|
+
const root = fs.realpathSync(repositoryRoot)
|
|
95
|
+
const node = getFileIdentity(process.execPath)
|
|
96
|
+
const runner = getFileIdentity(command.argv[1])
|
|
97
|
+
if (!isPathInside(root, runner.path)) {
|
|
98
|
+
throw new Error(`Test-runner executable resolves outside repository.root: ${command.argv[1]}`)
|
|
199
99
|
}
|
|
200
|
-
return commands
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Returns the command executed by an env wrapper with its effective PATH and working directory.
|
|
205
|
-
*
|
|
206
|
-
* @param {object} command env wrapper command
|
|
207
|
-
* @returns {object} delegated command
|
|
208
|
-
*/
|
|
209
|
-
function getEnvDelegatedCommand (command) {
|
|
210
|
-
const parsed = parseArgv(command.argv)
|
|
211
|
-
if (parsed.unsupportedEnvOption) {
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Cannot approve env-wrapped command because option "${parsed.unsupportedEnvOption}" prevents reliable ` +
|
|
214
|
-
'executable fingerprinting.'
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
if (parsed.commandIndex >= command.argv.length) {
|
|
218
|
-
throw new Error('Cannot approve env-wrapped command because it does not identify a delegated executable.')
|
|
219
|
-
}
|
|
220
|
-
const delegatedExecutable = command.argv[parsed.commandIndex]
|
|
221
|
-
const requiresPathLookup = !isExplicitExecutablePath(delegatedExecutable)
|
|
222
|
-
if (requiresPathLookup && parsed.unsetEnvNames.some(name => name.toUpperCase() === 'PATH')) {
|
|
223
|
-
throw new Error('Cannot approve env-wrapped command because it removes PATH before selecting its executable.')
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const env = parsed.ignoreEnvironment ? { ...parsed.prefixEnv } : { ...command.env, ...parsed.prefixEnv }
|
|
227
|
-
if (requiresPathLookup && parsed.ignoreEnvironment && !Object.hasOwn(env, 'PATH')) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
'Cannot approve env-wrapped command because it clears the environment without declaring an explicit PATH.'
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
100
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
101
|
+
argv0: process.execPath,
|
|
102
|
+
path: node.path,
|
|
103
|
+
sha256: node.sha256,
|
|
104
|
+
repositoryRoot,
|
|
105
|
+
entrypoints: [runner],
|
|
238
106
|
}
|
|
239
107
|
}
|
|
240
108
|
|
|
241
109
|
/**
|
|
242
|
-
*
|
|
110
|
+
* Compares current and approved executable identities.
|
|
243
111
|
*
|
|
244
|
-
* @param {
|
|
245
|
-
* @
|
|
112
|
+
* @param {object} current current identity
|
|
113
|
+
* @param {object} approved approved identity
|
|
114
|
+
* @returns {boolean} exact identity match
|
|
246
115
|
*/
|
|
247
|
-
function
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
116
|
+
function areExecutableIdentitiesEqual (current, approved) {
|
|
117
|
+
return current.argv0 === approved.argv0 &&
|
|
118
|
+
current.path === approved.path &&
|
|
119
|
+
current.sha256 === approved.sha256 &&
|
|
120
|
+
current.entrypoints?.length === approved.entrypoints?.length &&
|
|
121
|
+
current.entrypoints.every((entrypoint, index) => {
|
|
122
|
+
const expected = approved.entrypoints[index]
|
|
123
|
+
return entrypoint.path === expected.path && entrypoint.sha256 === expected.sha256
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Splits NODE_OPTIONS without invoking a shell or expanding variables.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} source NODE_OPTIONS source
|
|
131
|
+
* @returns {string[]} Node.js arguments
|
|
132
|
+
*/
|
|
133
|
+
function splitNodeOptions (source) {
|
|
134
|
+
const args = []
|
|
135
|
+
let value = ''
|
|
136
|
+
let started = false
|
|
137
|
+
let quote
|
|
138
|
+
|
|
139
|
+
for (let index = 0; index < String(source).length; index++) {
|
|
140
|
+
const character = source[index]
|
|
141
|
+
if (character === '\0') throw new Error('NODE_OPTIONS contains a null byte.')
|
|
142
|
+
if (!quote && /\s/.test(character)) {
|
|
143
|
+
if (started) args.push(value)
|
|
144
|
+
value = ''
|
|
145
|
+
started = false
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (character === '"' || character === "'") {
|
|
149
|
+
if (!quote) {
|
|
150
|
+
quote = character
|
|
151
|
+
started = true
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (quote === character) {
|
|
155
|
+
quote = undefined
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const next = source[index + 1]
|
|
160
|
+
if (character === '\\' && next && (next === quote || /[\s'"\\]/.test(next))) {
|
|
161
|
+
value += source[++index]
|
|
162
|
+
} else {
|
|
163
|
+
value += character
|
|
164
|
+
}
|
|
165
|
+
started = true
|
|
252
166
|
}
|
|
167
|
+
if (quote) throw new Error('NODE_OPTIONS contains an unterminated quoted value.')
|
|
168
|
+
if (started) args.push(value)
|
|
169
|
+
return args
|
|
253
170
|
}
|
|
254
171
|
|
|
255
172
|
/**
|
|
256
|
-
*
|
|
173
|
+
* Returns whether a value is an explicit filesystem executable path.
|
|
257
174
|
*
|
|
258
|
-
* @param {
|
|
259
|
-
* @param {
|
|
260
|
-
* @returns {
|
|
175
|
+
* @param {string} value candidate value
|
|
176
|
+
* @param {string} [platform] target platform
|
|
177
|
+
* @returns {boolean} whether the value includes explicit path syntax
|
|
261
178
|
*/
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
const cached = identitiesByPath?.get(canonicalPath)
|
|
267
|
-
if (cached) return { invocationPath: resolved, ...cached }
|
|
268
|
-
const stat = fs.statSync(canonicalPath)
|
|
269
|
-
if (!stat.isFile()) return
|
|
270
|
-
const canonicalIdentity = {
|
|
271
|
-
path: canonicalPath,
|
|
272
|
-
sha256: crypto.createHash('sha256').update(fs.readFileSync(canonicalPath)).digest('hex'),
|
|
273
|
-
}
|
|
274
|
-
identitiesByPath?.set(canonicalPath, canonicalIdentity)
|
|
275
|
-
return { invocationPath: resolved, ...canonicalIdentity }
|
|
179
|
+
function isExplicitExecutablePath (value, platform = process.platform) {
|
|
180
|
+
if (typeof value !== 'string') return false
|
|
181
|
+
if (path.isAbsolute(value) || value.startsWith('.')) return true
|
|
182
|
+
return platform === 'win32' ? /[\\/]/.test(value) : value.includes('/')
|
|
276
183
|
}
|
|
277
184
|
|
|
278
185
|
/**
|
|
279
|
-
*
|
|
186
|
+
* Returns whether a command has the only executable shape the validator supports.
|
|
280
187
|
*
|
|
281
|
-
* @param {object}
|
|
282
|
-
* @returns {
|
|
188
|
+
* @param {object} command command candidate
|
|
189
|
+
* @returns {boolean} whether it is a direct runner
|
|
283
190
|
*/
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
commands.push([`${prefix}:basic-reporting`, getLocalValidationCommand(framework, basicSource), basicSource])
|
|
291
|
-
}
|
|
292
|
-
if (framework.ciWiringCommand) {
|
|
293
|
-
commands.push([`${prefix}:ci-wiring`, getCiWiringCommand(framework), framework.ciWiringCommand])
|
|
294
|
-
}
|
|
295
|
-
for (const [index, command] of (framework.setup?.commands || []).entries()) {
|
|
296
|
-
commands.push([`${prefix}:setup:${index}`, command, command])
|
|
297
|
-
}
|
|
298
|
-
for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
|
|
299
|
-
if (scenario.runCommand) {
|
|
300
|
-
commands.push([
|
|
301
|
-
`${prefix}:generated:${index}`,
|
|
302
|
-
getLocalValidationCommand(framework, scenario.runCommand),
|
|
303
|
-
scenario.runCommand,
|
|
304
|
-
])
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return commands
|
|
191
|
+
function isDirectRunnerCommand (command) {
|
|
192
|
+
return command?.usesShell === false &&
|
|
193
|
+
Array.isArray(command.argv) &&
|
|
194
|
+
command.argv.length >= 3 &&
|
|
195
|
+
command.argv[0] === process.execPath &&
|
|
196
|
+
path.isAbsolute(command.argv[1])
|
|
309
197
|
}
|
|
310
198
|
|
|
311
199
|
/**
|
|
312
|
-
*
|
|
200
|
+
* Fingerprints a regular file.
|
|
313
201
|
*
|
|
314
|
-
* @param {string}
|
|
315
|
-
* @
|
|
316
|
-
* @returns {boolean} whether the value is a path rather than a PATH name
|
|
202
|
+
* @param {string} filename file path
|
|
203
|
+
* @returns {{path: string, sha256: string}} file identity
|
|
317
204
|
*/
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
205
|
+
function getFileIdentity (filename) {
|
|
206
|
+
const physical = fs.realpathSync(filename)
|
|
207
|
+
const stat = fs.lstatSync(physical)
|
|
208
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Executable must be a regular file: ${filename}`)
|
|
209
|
+
return {
|
|
210
|
+
path: physical,
|
|
211
|
+
sha256: crypto.createHash('sha256').update(fs.readFileSync(physical)).digest('hex'),
|
|
212
|
+
}
|
|
325
213
|
}
|
|
326
214
|
|
|
327
215
|
/**
|
|
328
|
-
*
|
|
216
|
+
* Returns whether a path resolves to a regular file.
|
|
329
217
|
*
|
|
330
|
-
* @param {string} filename
|
|
331
|
-
* @returns {boolean} whether the
|
|
218
|
+
* @param {string} filename candidate path
|
|
219
|
+
* @returns {boolean} whether the file is available
|
|
332
220
|
*/
|
|
333
|
-
function
|
|
221
|
+
function isRegularFile (filename) {
|
|
334
222
|
try {
|
|
335
|
-
fs.
|
|
336
|
-
return true
|
|
223
|
+
return fs.statSync(filename).isFile()
|
|
337
224
|
} catch {
|
|
338
225
|
return false
|
|
339
226
|
}
|
|
340
227
|
}
|
|
341
228
|
|
|
342
229
|
/**
|
|
343
|
-
*
|
|
230
|
+
* Checks path containment.
|
|
344
231
|
*
|
|
345
|
-
* @param {string
|
|
346
|
-
* @
|
|
232
|
+
* @param {string} root root path
|
|
233
|
+
* @param {string} filename candidate path
|
|
234
|
+
* @returns {boolean} whether the path is contained
|
|
347
235
|
*/
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
prefixAssignments: [],
|
|
352
|
-
prefixEnv: {},
|
|
353
|
-
unsetEnvNames: [],
|
|
354
|
-
commandIndex: 0,
|
|
355
|
-
corepackIndex: -1,
|
|
356
|
-
pathAdjusted: false,
|
|
357
|
-
unsupportedEnvOption: undefined,
|
|
358
|
-
workingDirectory: undefined,
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (!Array.isArray(argv) || argv.length === 0) return result
|
|
362
|
-
|
|
363
|
-
let index = 0
|
|
364
|
-
if (isEnvExecutable(argv[index])) {
|
|
365
|
-
index++
|
|
366
|
-
while (index < argv.length) {
|
|
367
|
-
const option = argv[index]
|
|
368
|
-
if (option === '--') {
|
|
369
|
-
index++
|
|
370
|
-
break
|
|
371
|
-
}
|
|
372
|
-
if (option === '-' || option === '-i' || option === '--ignore-environment') {
|
|
373
|
-
result.ignoreEnvironment = true
|
|
374
|
-
index++
|
|
375
|
-
continue
|
|
376
|
-
}
|
|
377
|
-
if (option === '-u' || option === '--unset') {
|
|
378
|
-
if (typeof argv[index + 1] === 'string') result.unsetEnvNames.push(argv[index + 1])
|
|
379
|
-
index += 2
|
|
380
|
-
continue
|
|
381
|
-
}
|
|
382
|
-
const unsetMatch = /^(?:-u|--unset=)(.+)$/.exec(option)
|
|
383
|
-
if (unsetMatch) {
|
|
384
|
-
result.unsetEnvNames.push(unsetMatch[1])
|
|
385
|
-
index++
|
|
386
|
-
continue
|
|
387
|
-
}
|
|
388
|
-
if (option === '-C' || option === '--chdir') {
|
|
389
|
-
if (typeof argv[index + 1] !== 'string') {
|
|
390
|
-
result.unsupportedEnvOption = option
|
|
391
|
-
break
|
|
392
|
-
}
|
|
393
|
-
result.workingDirectory = argv[index + 1]
|
|
394
|
-
index += 2
|
|
395
|
-
continue
|
|
396
|
-
}
|
|
397
|
-
const chdirMatch = /^(?:-C(.+)|--chdir=(.+))$/.exec(option)
|
|
398
|
-
if (chdirMatch) {
|
|
399
|
-
result.workingDirectory = chdirMatch[1] || chdirMatch[2]
|
|
400
|
-
index++
|
|
401
|
-
continue
|
|
402
|
-
}
|
|
403
|
-
if (option === '-S' || option === '--split-string' || /^(?:-S.+|--split-string=.+)$/.test(option)) {
|
|
404
|
-
result.unsupportedEnvOption = option
|
|
405
|
-
break
|
|
406
|
-
}
|
|
407
|
-
if (isSupportedEnvFlag(option)) {
|
|
408
|
-
index++
|
|
409
|
-
continue
|
|
410
|
-
}
|
|
411
|
-
if (option.startsWith('-')) {
|
|
412
|
-
result.unsupportedEnvOption = option
|
|
413
|
-
break
|
|
414
|
-
}
|
|
415
|
-
if (!isEnvAssignment(option)) break
|
|
416
|
-
|
|
417
|
-
const assignment = argv[index]
|
|
418
|
-
const equalsIndex = assignment.indexOf('=')
|
|
419
|
-
const name = assignment.slice(0, equalsIndex)
|
|
420
|
-
const value = assignment.slice(equalsIndex + 1)
|
|
421
|
-
result.prefixEnv[name] = value
|
|
422
|
-
|
|
423
|
-
if (name === 'PATH') {
|
|
424
|
-
result.pathAdjusted = true
|
|
425
|
-
} else {
|
|
426
|
-
result.prefixAssignments.push(assignment)
|
|
427
|
-
}
|
|
428
|
-
index++
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
result.commandIndex = index
|
|
433
|
-
|
|
434
|
-
if (isNodeExecutable(argv[index]) && isCorepackScript(argv[index + 1]) && argv[index + 2]) {
|
|
435
|
-
result.corepackIndex = index + 1
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return result
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function isSupportedEnvFlag (option) {
|
|
442
|
-
return /^(?:-0|-v|--null|--debug|--help|--version|--list-signal-handling)$/.test(option) ||
|
|
443
|
-
/^--(?:block|default|ignore)-signal(?:=.*)?$/.test(option)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function isEnvExecutable (value) {
|
|
447
|
-
const name = getExecutableName(value)
|
|
448
|
-
return name === 'env' || name === 'env.exe'
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function isEnvAssignment (value) {
|
|
452
|
-
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function isNodeExecutable (value = '') {
|
|
456
|
-
const name = getExecutableName(value)
|
|
457
|
-
return name === 'node' || name === 'node.exe'
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function isCorepackScript (value = '') {
|
|
461
|
-
const name = getExecutableName(value)
|
|
462
|
-
return name === 'corepack' || name === 'corepack.exe' || name === 'corepack.js'
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function getExecutableName (value = '') {
|
|
466
|
-
return String(value).split(/[\\/]/).pop().toLowerCase()
|
|
236
|
+
function isPathInside (root, filename) {
|
|
237
|
+
const relative = path.relative(path.resolve(root), path.resolve(filename))
|
|
238
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
467
239
|
}
|
|
468
240
|
|
|
469
241
|
module.exports = {
|
|
242
|
+
areExecutableIdentitiesEqual,
|
|
470
243
|
bindManifestExecutables,
|
|
471
|
-
|
|
244
|
+
getCommandExecutableIdentity,
|
|
472
245
|
getExecutableForSpawn,
|
|
473
246
|
getManifestCommands,
|
|
474
247
|
getResolvedExecutable,
|
|
475
248
|
getUnavailableExecutable,
|
|
476
|
-
isEnvExecutable,
|
|
477
249
|
isExplicitExecutablePath,
|
|
478
|
-
|
|
479
|
-
parseArgv,
|
|
250
|
+
splitNodeOptions,
|
|
480
251
|
}
|