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
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
/* eslint-disable no-console, eslint-rules/eslint-process-env */
|
|
4
4
|
|
|
5
|
+
const fs = require('node:fs')
|
|
5
6
|
const path = require('path')
|
|
6
|
-
const { spawn } = require('child_process')
|
|
7
|
+
const { spawn, spawnSync } = require('child_process')
|
|
7
8
|
|
|
8
|
-
const { cleanupCommandOutputs, prepareCommandOutputs } = require('./command-output-policy')
|
|
9
9
|
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} = require('./executable')
|
|
10
|
+
cleanupCommandOutputs,
|
|
11
|
+
prepareCommandOutputs,
|
|
12
|
+
} = require('./command-output-policy')
|
|
13
|
+
const { getExecutableForSpawn } = require('./executable')
|
|
14
|
+
const {
|
|
15
|
+
environmentNamesEqual,
|
|
16
|
+
getEnvironmentValue,
|
|
17
|
+
isDatadogEnvironmentName,
|
|
18
|
+
mergeEnvironment,
|
|
19
|
+
setEnvironmentValue,
|
|
20
|
+
} = require('./environment')
|
|
14
21
|
const { sanitizeConsoleText, sanitizeString } = require('./redaction')
|
|
15
22
|
const { ensureSafeDirectory, writeFileSafely } = require('./safe-files')
|
|
16
23
|
|
|
@@ -78,6 +85,7 @@ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024
|
|
|
78
85
|
const EARLY_STOP_KILL_GRACE_MS = 500
|
|
79
86
|
const TIMEOUT_KILL_GRACE_MS = 5000
|
|
80
87
|
const TIMEOUT_FINALIZE_GRACE_MS = 1000
|
|
88
|
+
const WINDOWS_TASKKILL_TIMEOUT_MS = 10_000
|
|
81
89
|
|
|
82
90
|
function runCommand (command, options = {}) {
|
|
83
91
|
const {
|
|
@@ -110,8 +118,10 @@ function runCommand (command, options = {}) {
|
|
|
110
118
|
durationMs: 0,
|
|
111
119
|
timedOut: false,
|
|
112
120
|
stdout: '',
|
|
121
|
+
stdoutOmittedBytes: 0,
|
|
113
122
|
stdoutTruncated: false,
|
|
114
123
|
stderr: '',
|
|
124
|
+
stderrOmittedBytes: 0,
|
|
115
125
|
stderrTruncated: false,
|
|
116
126
|
artifacts: {},
|
|
117
127
|
}
|
|
@@ -122,51 +132,64 @@ function runCommand (command, options = {}) {
|
|
|
122
132
|
} catch (error) {
|
|
123
133
|
return Promise.reject(error)
|
|
124
134
|
}
|
|
135
|
+
const missingRequiredEnvVars = getMissingRequiredEnvironmentNames(command.requiredEnvVars)
|
|
136
|
+
if (missingRequiredEnvVars.length > 0) {
|
|
137
|
+
result.missingRequiredEnvVars = missingRequiredEnvVars
|
|
138
|
+
result.stderr = 'The approved command requires environment variables that are not available: ' +
|
|
139
|
+
`${missingRequiredEnvVars.join(', ')}.\n`
|
|
140
|
+
result.durationMs = Date.now() - startedAt
|
|
141
|
+
return Promise.resolve(result)
|
|
142
|
+
}
|
|
125
143
|
const outputStates = prepareCommandOutputs({ command, artifactRoot, outDir, repositoryRoot })
|
|
126
144
|
|
|
127
145
|
return new Promise((resolve) => {
|
|
128
146
|
let finalized = false
|
|
129
|
-
let
|
|
147
|
+
let processTreeCleanupPending = false
|
|
130
148
|
let pendingCloseResult
|
|
131
|
-
const childEnv = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
childEnv
|
|
149
|
+
const childEnv = { ...getBaseEnv(envMode, command.requiredEnvVars) }
|
|
150
|
+
mergeEnvironment(childEnv, command.env)
|
|
151
|
+
mergeEnvironment(childEnv, env)
|
|
152
|
+
const commandNodeOptions = getEnvironmentValue(command.env || {}, 'NODE_OPTIONS')
|
|
153
|
+
const injectedNodeOptions = getEnvironmentValue(env, 'NODE_OPTIONS')
|
|
154
|
+
if (commandNodeOptions && injectedNodeOptions) {
|
|
155
|
+
setEnvironmentValue(childEnv, 'NODE_OPTIONS', mergeNodeOptions(injectedNodeOptions, commandNodeOptions))
|
|
138
156
|
}
|
|
139
157
|
for (const [name, value] of Object.entries(childEnv)) {
|
|
140
158
|
if (value === undefined) delete childEnv[name]
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
const useProcessGroup = shouldUseProcessGroup()
|
|
162
|
+
const windowsTaskkillPath = getWindowsTaskkillPath()
|
|
163
|
+
if (process.platform === 'win32' && !windowsTaskkillPath) {
|
|
164
|
+
result.stderr = 'Validation cannot safely bound test processes because the Windows process-tree cleanup ' +
|
|
165
|
+
'executable is unavailable.\n'
|
|
166
|
+
result.durationMs = Date.now() - startedAt
|
|
167
|
+
try {
|
|
168
|
+
finishCommandOutputCleanup(result, outputStates)
|
|
169
|
+
} catch (cleanupError) {
|
|
170
|
+
result.outputCleanupError = cleanupError?.message || String(cleanupError)
|
|
171
|
+
}
|
|
172
|
+
resolve(result)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const managesProcessTree = useProcessGroup || Boolean(windowsTaskkillPath)
|
|
144
176
|
let child
|
|
145
177
|
try {
|
|
146
178
|
const executable = getExecutableForSpawn(command, { requireApproval: requireExecutableApproval })
|
|
147
179
|
const argv0 = process.platform === 'win32' ? {} : { argv0: executable.argv0 }
|
|
148
|
-
child = command.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
})
|
|
157
|
-
: spawn(executable.path, command.argv.slice(1), {
|
|
158
|
-
...argv0,
|
|
159
|
-
cwd: command.cwd,
|
|
160
|
-
detached: useProcessGroup,
|
|
161
|
-
env: childEnv,
|
|
162
|
-
shell: false,
|
|
163
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
|
-
})
|
|
180
|
+
child = spawn(executable.path, command.argv.slice(1), {
|
|
181
|
+
...argv0,
|
|
182
|
+
cwd: command.cwd,
|
|
183
|
+
detached: useProcessGroup,
|
|
184
|
+
env: childEnv,
|
|
185
|
+
shell: false,
|
|
186
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
187
|
+
})
|
|
165
188
|
} catch (error) {
|
|
166
189
|
result.stderr = `${error.stack || error}\n`
|
|
167
190
|
result.durationMs = Date.now() - startedAt
|
|
168
191
|
try {
|
|
169
|
-
result
|
|
192
|
+
finishCommandOutputCleanup(result, outputStates)
|
|
170
193
|
} catch (cleanupError) {
|
|
171
194
|
result.outputCleanupError = cleanupError?.message || String(cleanupError)
|
|
172
195
|
}
|
|
@@ -183,13 +206,15 @@ function runCommand (command, options = {}) {
|
|
|
183
206
|
let killTimer
|
|
184
207
|
let finalizeTimer
|
|
185
208
|
let stopTimer
|
|
209
|
+
let stdoutCapture
|
|
210
|
+
let stderrCapture
|
|
186
211
|
const timeout = setTimeout(() => {
|
|
187
212
|
result.timedOut = true
|
|
188
|
-
|
|
189
|
-
signalChild(child, 'SIGTERM', useProcessGroup)
|
|
213
|
+
processTreeCleanupPending = managesProcessTree
|
|
214
|
+
signalChild(child, 'SIGTERM', useProcessGroup, windowsTaskkillPath)
|
|
190
215
|
killTimer = setTimeout(() => {
|
|
191
|
-
signalChild(child, 'SIGKILL', useProcessGroup)
|
|
192
|
-
|
|
216
|
+
signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
|
|
217
|
+
finishProcessTreeCleanup('SIGKILL')
|
|
193
218
|
}, timeoutKillGraceMs)
|
|
194
219
|
}, timeoutMs)
|
|
195
220
|
|
|
@@ -203,39 +228,45 @@ function runCommand (command, options = {}) {
|
|
|
203
228
|
|
|
204
229
|
clearInterval(stopTimer)
|
|
205
230
|
result.stoppedEarly = true
|
|
206
|
-
|
|
207
|
-
signalChild(child, 'SIGTERM', useProcessGroup)
|
|
231
|
+
processTreeCleanupPending = managesProcessTree
|
|
232
|
+
signalChild(child, 'SIGTERM', useProcessGroup, windowsTaskkillPath)
|
|
208
233
|
killTimer = setTimeout(() => {
|
|
209
|
-
signalChild(child, 'SIGKILL', useProcessGroup)
|
|
210
|
-
|
|
234
|
+
signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
|
|
235
|
+
finishProcessTreeCleanup('SIGKILL')
|
|
211
236
|
}, EARLY_STOP_KILL_GRACE_MS)
|
|
212
237
|
}, 25)
|
|
213
238
|
}
|
|
214
239
|
|
|
215
240
|
child.stdout.on('data', chunk => {
|
|
216
|
-
const capture = appendCapturedOutput(
|
|
241
|
+
const capture = appendCapturedOutput(stdoutCapture, chunk, maxOutputBytes)
|
|
242
|
+
stdoutCapture = capture
|
|
217
243
|
result.stdout = capture.output
|
|
218
|
-
result.
|
|
244
|
+
result.stdoutOmittedBytes = capture.omittedBytes
|
|
245
|
+
result.stdoutTruncated = capture.truncated
|
|
219
246
|
})
|
|
220
247
|
child.stderr.on('data', chunk => {
|
|
221
|
-
const capture = appendCapturedOutput(
|
|
248
|
+
const capture = appendCapturedOutput(stderrCapture, chunk, maxOutputBytes)
|
|
249
|
+
stderrCapture = capture
|
|
222
250
|
result.stderr = capture.output
|
|
223
|
-
result.
|
|
251
|
+
result.stderrOmittedBytes = capture.omittedBytes
|
|
252
|
+
result.stderrTruncated = capture.truncated
|
|
224
253
|
})
|
|
225
254
|
child.on('error', err => {
|
|
226
255
|
result.stderr += `${err.stack || err}\n`
|
|
227
256
|
finalize(null, null)
|
|
228
257
|
})
|
|
229
258
|
child.on('close', (code, signal) => {
|
|
230
|
-
if (
|
|
259
|
+
if (processTreeCleanupPending) {
|
|
231
260
|
pendingCloseResult = { code, signal }
|
|
261
|
+
signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
|
|
262
|
+
finishProcessTreeCleanup(signal)
|
|
232
263
|
return
|
|
233
264
|
}
|
|
234
265
|
finalize(code, signal)
|
|
235
266
|
})
|
|
236
267
|
|
|
237
|
-
function
|
|
238
|
-
|
|
268
|
+
function finishProcessTreeCleanup (fallbackSignal) {
|
|
269
|
+
processTreeCleanupPending = false
|
|
239
270
|
if (pendingCloseResult) {
|
|
240
271
|
finalize(pendingCloseResult.code, pendingCloseResult.signal || fallbackSignal)
|
|
241
272
|
return
|
|
@@ -255,7 +286,7 @@ function runCommand (command, options = {}) {
|
|
|
255
286
|
result.durationMs = Date.now() - startedAt
|
|
256
287
|
|
|
257
288
|
try {
|
|
258
|
-
result
|
|
289
|
+
finishCommandOutputCleanup(result, outputStates)
|
|
259
290
|
} catch (err) {
|
|
260
291
|
result.outputCleanupError = err && err.message ? err.message : String(err)
|
|
261
292
|
result.stderr += '\n[test-optimization-validator] could not clean up command outputs: ' +
|
|
@@ -271,13 +302,13 @@ function runCommand (command, options = {}) {
|
|
|
271
302
|
writeFileSafely(
|
|
272
303
|
artifactRoot,
|
|
273
304
|
result.artifacts.stdout,
|
|
274
|
-
sanitizeString(
|
|
305
|
+
sanitizeString(result.stdout),
|
|
275
306
|
'command stdout artifact'
|
|
276
307
|
)
|
|
277
308
|
writeFileSafely(
|
|
278
309
|
artifactRoot,
|
|
279
310
|
result.artifacts.stderr,
|
|
280
|
-
sanitizeString(
|
|
311
|
+
sanitizeString(result.stderr),
|
|
281
312
|
'command stderr artifact'
|
|
282
313
|
)
|
|
283
314
|
writeFileSafely(artifactRoot, result.artifacts.command, `${JSON.stringify({
|
|
@@ -291,7 +322,9 @@ function runCommand (command, options = {}) {
|
|
|
291
322
|
timedOut: result.timedOut,
|
|
292
323
|
stoppedEarly: result.stoppedEarly,
|
|
293
324
|
stdoutTruncated: result.stdoutTruncated,
|
|
325
|
+
stdoutOmittedBytes: result.stdoutOmittedBytes,
|
|
294
326
|
stderrTruncated: result.stderrTruncated,
|
|
327
|
+
stderrOmittedBytes: result.stderrOmittedBytes,
|
|
295
328
|
maxOutputBytes,
|
|
296
329
|
commandOutputPaths: result.commandOutputPaths,
|
|
297
330
|
outputCleanupError: result.outputCleanupError,
|
|
@@ -308,6 +341,17 @@ function runCommand (command, options = {}) {
|
|
|
308
341
|
})
|
|
309
342
|
}
|
|
310
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Cleans command outputs after the command exits.
|
|
346
|
+
*
|
|
347
|
+
* @param {object} result command result
|
|
348
|
+
* @param {object[]} outputStates prepared command output state
|
|
349
|
+
* @returns {void}
|
|
350
|
+
*/
|
|
351
|
+
function finishCommandOutputCleanup (result, outputStates) {
|
|
352
|
+
result.commandOutputPaths = cleanupCommandOutputs(outputStates)
|
|
353
|
+
}
|
|
354
|
+
|
|
311
355
|
/**
|
|
312
356
|
* Returns the effective bounded execution settings used for one project command.
|
|
313
357
|
*
|
|
@@ -327,48 +371,92 @@ function getCommandExecutionSettings (command) {
|
|
|
327
371
|
/**
|
|
328
372
|
* Appends output while retaining only the latest bytes for diagnostic artifacts.
|
|
329
373
|
*
|
|
330
|
-
* @param {
|
|
374
|
+
* @param {object|undefined} current currently captured output state
|
|
331
375
|
* @param {Buffer|string} chunk new output chunk
|
|
332
376
|
* @param {number} maxBytes maximum retained bytes
|
|
333
377
|
* @returns {{output: string, truncated: boolean}} retained output and truncation flag
|
|
334
378
|
*/
|
|
335
379
|
function appendCapturedOutput (current, chunk, maxBytes) {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
380
|
+
const nextChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
|
|
381
|
+
const totalBytes = (current?.totalBytes || 0) + nextChunk.length
|
|
382
|
+
const headLimit = Math.floor(maxBytes / 2)
|
|
383
|
+
const tailLimit = maxBytes - headLimit
|
|
384
|
+
let head = current?.head || Buffer.alloc(0)
|
|
385
|
+
let tail
|
|
386
|
+
|
|
387
|
+
if (current?.truncated) {
|
|
388
|
+
const combinedTail = Buffer.concat([current.tail, nextChunk])
|
|
389
|
+
tail = combinedTail.subarray(Math.max(0, combinedTail.length - tailLimit))
|
|
390
|
+
} else {
|
|
391
|
+
const combined = Buffer.concat([current?.tail || Buffer.alloc(0), nextChunk])
|
|
392
|
+
if (combined.length <= maxBytes) {
|
|
393
|
+
return {
|
|
394
|
+
head,
|
|
395
|
+
tail: combined,
|
|
396
|
+
totalBytes,
|
|
397
|
+
omittedBytes: 0,
|
|
398
|
+
output: combined.toString('utf8'),
|
|
399
|
+
truncated: false,
|
|
400
|
+
}
|
|
345
401
|
}
|
|
402
|
+
head = combined.subarray(0, headLimit)
|
|
403
|
+
tail = combined.subarray(combined.length - tailLimit)
|
|
346
404
|
}
|
|
347
405
|
|
|
406
|
+
const omittedBytes = totalBytes - head.length - tail.length
|
|
348
407
|
return {
|
|
349
|
-
|
|
408
|
+
head,
|
|
409
|
+
tail,
|
|
410
|
+
totalBytes,
|
|
411
|
+
omittedBytes,
|
|
412
|
+
output: `${head.toString('utf8')}\n[test-optimization-validator] ${omittedBytes} bytes omitted\n` +
|
|
413
|
+
tail.toString('utf8'),
|
|
350
414
|
truncated: true,
|
|
351
415
|
}
|
|
352
416
|
}
|
|
353
417
|
|
|
418
|
+
function shouldUseProcessGroup () {
|
|
419
|
+
return process.platform !== 'win32'
|
|
420
|
+
}
|
|
421
|
+
|
|
354
422
|
/**
|
|
355
|
-
*
|
|
423
|
+
* Resolves the fixed Windows process-tree cleanup executable without PATH lookup.
|
|
356
424
|
*
|
|
357
|
-
* @
|
|
358
|
-
* @param {boolean} truncated whether earlier output was omitted
|
|
359
|
-
* @param {number} maxBytes maximum retained bytes
|
|
360
|
-
* @returns {string} output artifact content
|
|
425
|
+
* @returns {string|undefined} physical taskkill executable
|
|
361
426
|
*/
|
|
362
|
-
function
|
|
363
|
-
if (
|
|
364
|
-
return `[test-optimization-validator] output truncated to last ${maxBytes} bytes\n${output}`
|
|
365
|
-
}
|
|
427
|
+
function getWindowsTaskkillPath () {
|
|
428
|
+
if (process.platform !== 'win32') return
|
|
366
429
|
|
|
367
|
-
|
|
368
|
-
|
|
430
|
+
const systemRoot = getEnvironmentValue(process.env, 'SystemRoot') ||
|
|
431
|
+
getEnvironmentValue(process.env, 'WINDIR')
|
|
432
|
+
if (!systemRoot || !path.win32.isAbsolute(systemRoot)) return
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const root = fs.realpathSync(systemRoot)
|
|
436
|
+
const candidate = path.win32.join(root, 'System32', 'taskkill.exe')
|
|
437
|
+
const stat = fs.lstatSync(candidate)
|
|
438
|
+
const physical = fs.realpathSync(candidate)
|
|
439
|
+
const relative = path.win32.relative(root, physical)
|
|
440
|
+
if (!stat.isFile() || stat.isSymbolicLink() ||
|
|
441
|
+
relative.startsWith('..') || path.win32.isAbsolute(relative)) return
|
|
442
|
+
return physical
|
|
443
|
+
} catch {}
|
|
369
444
|
}
|
|
370
445
|
|
|
371
|
-
function signalChild (child, signal, useProcessGroup) {
|
|
446
|
+
function signalChild (child, signal, useProcessGroup, windowsTaskkillPath) {
|
|
447
|
+
if (windowsTaskkillPath) {
|
|
448
|
+
try {
|
|
449
|
+
const args = ['/PID', String(child.pid), '/T', '/F']
|
|
450
|
+
const outcome = spawnSync(windowsTaskkillPath, args, {
|
|
451
|
+
shell: false,
|
|
452
|
+
stdio: 'ignore',
|
|
453
|
+
timeout: WINDOWS_TASKKILL_TIMEOUT_MS,
|
|
454
|
+
windowsHide: true,
|
|
455
|
+
})
|
|
456
|
+
if (!outcome.error && outcome.status === 0) return
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
|
|
372
460
|
try {
|
|
373
461
|
if (useProcessGroup) {
|
|
374
462
|
process.kill(-child.pid, signal)
|
|
@@ -379,16 +467,29 @@ function signalChild (child, signal, useProcessGroup) {
|
|
|
379
467
|
child.kill(signal)
|
|
380
468
|
}
|
|
381
469
|
|
|
382
|
-
function getBaseEnv (envMode) {
|
|
470
|
+
function getBaseEnv (envMode, requiredEnvVars = []) {
|
|
383
471
|
if (envMode !== 'clean') return process.env
|
|
384
472
|
|
|
385
473
|
const cleanEnv = {}
|
|
386
|
-
for (const name of CLEAN_ENV_ALLOWLIST) {
|
|
387
|
-
|
|
474
|
+
for (const name of [...CLEAN_ENV_ALLOWLIST, ...requiredEnvVars]) {
|
|
475
|
+
const entry = Object.entries(process.env).find(([candidate]) => {
|
|
476
|
+
return environmentNamesEqual(candidate, name)
|
|
477
|
+
})
|
|
478
|
+
if (entry) setEnvironmentValue(cleanEnv, name, entry[1])
|
|
388
479
|
}
|
|
389
480
|
return cleanEnv
|
|
390
481
|
}
|
|
391
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Finds explicitly approved ambient variables that are unavailable at execution time.
|
|
485
|
+
*
|
|
486
|
+
* @param {string[]|undefined} requiredEnvVars approved variable names
|
|
487
|
+
* @returns {string[]} missing names
|
|
488
|
+
*/
|
|
489
|
+
function getMissingRequiredEnvironmentNames (requiredEnvVars = []) {
|
|
490
|
+
return requiredEnvVars.filter(name => getEnvironmentValue(process.env, name) === undefined)
|
|
491
|
+
}
|
|
492
|
+
|
|
392
493
|
function buildDatadogEnv ({ fixture, outputRoot, scenario, framework }) {
|
|
393
494
|
const offline = buildOfflineValidationEnv({ fixture, outputRoot })
|
|
394
495
|
return {
|
|
@@ -405,7 +506,7 @@ function buildDatadogEnv ({ fixture, outputRoot, scenario, framework }) {
|
|
|
405
506
|
}
|
|
406
507
|
}
|
|
407
508
|
|
|
408
|
-
function
|
|
509
|
+
function buildOfflineCaptureEnv ({ fixture, outputRoot }) {
|
|
409
510
|
return {
|
|
410
511
|
...buildOfflineValidationEnv({ fixture, outputRoot }),
|
|
411
512
|
[VALIDATION_CAPTURE_MODE_ENV]: 'sample',
|
|
@@ -454,138 +555,15 @@ function buildOfflineValidationEnv ({ fixture, outputRoot }) {
|
|
|
454
555
|
*/
|
|
455
556
|
function assertNoInlineValidationEnvOverrides (command, env) {
|
|
456
557
|
if (!env[VALIDATION_MODE_ENV]) return
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
])
|
|
463
|
-
|
|
464
|
-
if (command.usesShell) {
|
|
465
|
-
rejectReservedShellAssignments(command.shellCommand, reservedEnvNames)
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const parsed = parseArgv(command.argv)
|
|
470
|
-
rejectReservedEnvSplitStrings(command.argv, reservedEnvNames)
|
|
471
|
-
if (parsed.ignoreEnvironment) throwEnvironmentReset()
|
|
472
|
-
if (parsed.unsupportedEnvOption) throwUnsupportedEnvOption(parsed.unsupportedEnvOption)
|
|
473
|
-
for (const name of Object.keys(parsed.prefixEnv)) {
|
|
474
|
-
if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
|
|
475
|
-
}
|
|
476
|
-
for (const name of parsed.unsetEnvNames) {
|
|
477
|
-
if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (isPosixShellExecutable(command.argv[parsed.commandIndex])) {
|
|
481
|
-
for (let index = parsed.commandIndex + 1; index < command.argv.length - 1; index++) {
|
|
482
|
-
const value = command.argv[index]
|
|
483
|
-
if (isShellCommandFlag(value) && typeof command.argv[index + 1] === 'string') {
|
|
484
|
-
rejectReservedShellAssignments(command.argv[index + 1], reservedEnvNames)
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Rejects reserved variable assignments and removals in shell source.
|
|
492
|
-
*
|
|
493
|
-
* @param {string} shellCommand shell source
|
|
494
|
-
* @param {Set<string>} reservedEnvNames validator-controlled environment names
|
|
495
|
-
*/
|
|
496
|
-
function rejectReservedShellAssignments (shellCommand, reservedEnvNames) {
|
|
497
|
-
const source = String(shellCommand || '')
|
|
498
|
-
const environmentReset =
|
|
499
|
-
/\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-(?=\s|$)|-i\b|--ignore-environment\b)/i
|
|
500
|
-
|
|
501
|
-
if (environmentReset.test(source)) throwEnvironmentReset()
|
|
502
|
-
|
|
503
|
-
for (const name of reservedEnvNames) {
|
|
504
|
-
const escapedName = escapeRegExp(name)
|
|
505
|
-
const assignment = new RegExp(
|
|
506
|
-
String.raw`(?:^|[\s;&|()'"])(?:export\s+|set\s+)?(?:\$env:)?${escapedName}\s*\+?=`,
|
|
507
|
-
'i'
|
|
508
|
-
)
|
|
509
|
-
const removal = new RegExp(
|
|
510
|
-
String.raw`(?:\bunset(?:\s+(?:-[A-Za-z]+|[A-Za-z_][A-Za-z0-9_]*))*\s+|` +
|
|
511
|
-
String.raw`\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-u\s*|--unset(?:=|\s+))|` +
|
|
512
|
-
String.raw`\bRemove-Item\s+(?:[^;&|]*\s)?env:)${escapedName}\b`,
|
|
513
|
-
'i'
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
if (assignment.test(source) || removal.test(source)) throwReservedEnvOverride(name)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Rejects reserved environment changes hidden inside env --split-string arguments.
|
|
522
|
-
*
|
|
523
|
-
* @param {string[]} argv structured command arguments
|
|
524
|
-
* @param {Set<string>} reservedEnvNames validator-controlled environment names
|
|
525
|
-
* @returns {void}
|
|
526
|
-
*/
|
|
527
|
-
function rejectReservedEnvSplitStrings (argv, reservedEnvNames) {
|
|
528
|
-
if (!Array.isArray(argv) || !isEnvExecutable(argv[0])) return
|
|
529
|
-
|
|
530
|
-
for (let index = 1; index < argv.length; index++) {
|
|
531
|
-
const argument = argv[index]
|
|
532
|
-
if (argument === '-S' || argument === '--split-string') {
|
|
533
|
-
if (typeof argv[index + 1] === 'string') {
|
|
534
|
-
rejectReservedShellAssignments(`env ${argv[index + 1]}`, reservedEnvNames)
|
|
535
|
-
}
|
|
536
|
-
index++
|
|
537
|
-
continue
|
|
558
|
+
for (const name of Object.keys(command.env || {})) {
|
|
559
|
+
const normalized = process.platform === 'win32' ? name.toUpperCase() : name
|
|
560
|
+
if (VALIDATION_RESERVED_ENV_NAMES.some(reserved => environmentNamesEqual(reserved, name)) ||
|
|
561
|
+
isDatadogEnvironmentName(name) || normalized.startsWith('OTEL_')) {
|
|
562
|
+
throw new Error(`Direct-runner adapter must not override validator-controlled environment variable ${name}.`)
|
|
538
563
|
}
|
|
539
|
-
|
|
540
|
-
const splitString = /^(?:-S|--split-string=)(.+)$/.exec(argument)?.[1]
|
|
541
|
-
if (splitString !== undefined) rejectReservedShellAssignments(`env ${splitString}`, reservedEnvNames)
|
|
542
564
|
}
|
|
543
565
|
}
|
|
544
566
|
|
|
545
|
-
function isShellCommandFlag (value) {
|
|
546
|
-
return /^-[A-Za-z]*c[A-Za-z]*$/.test(value)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function isPosixShellExecutable (value) {
|
|
550
|
-
return /^(?:a|ba|da|k|z)?sh$/.test(path.basename(String(value || '')).toLowerCase())
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Throws a customer-facing error for unsafe inline validation environment changes.
|
|
555
|
-
*
|
|
556
|
-
* @param {string} name reserved environment variable
|
|
557
|
-
*/
|
|
558
|
-
function throwReservedEnvOverride (name) {
|
|
559
|
-
throw new Error(
|
|
560
|
-
`Refusing inline ${name} changes during live validation because they can bypass the offline validation mode. ` +
|
|
561
|
-
'Record CI-provided values in command.env so the validator can apply its private diagnostic settings.'
|
|
562
|
-
)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function throwEnvironmentReset () {
|
|
566
|
-
throw new Error(
|
|
567
|
-
'Refusing to clear the command environment during live validation because this would remove the offline ' +
|
|
568
|
-
'validation and Datadog initialization settings.'
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function throwUnsupportedEnvOption (option) {
|
|
573
|
-
throw new Error(
|
|
574
|
-
`Refusing unsupported env option ${option} during live validation because its environment effects cannot be ` +
|
|
575
|
-
'verified safely.'
|
|
576
|
-
)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Escapes a literal for use in a regular expression.
|
|
581
|
-
*
|
|
582
|
-
* @param {string} value literal value
|
|
583
|
-
* @returns {string} escaped value
|
|
584
|
-
*/
|
|
585
|
-
function escapeRegExp (value) {
|
|
586
|
-
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
|
|
587
|
-
}
|
|
588
|
-
|
|
589
567
|
function withCiPreloads (nodeOptions = '', framework) {
|
|
590
568
|
let result = nodeOptions.trim()
|
|
591
569
|
|
|
@@ -621,7 +599,7 @@ function formatNodeRequire (filename) {
|
|
|
621
599
|
}
|
|
622
600
|
|
|
623
601
|
function serializeCommand (command) {
|
|
624
|
-
return command.
|
|
602
|
+
return command.argv.join(' ')
|
|
625
603
|
}
|
|
626
604
|
|
|
627
605
|
/**
|
|
@@ -631,7 +609,6 @@ function serializeCommand (command) {
|
|
|
631
609
|
* @returns {string} unambiguous customer-facing command
|
|
632
610
|
*/
|
|
633
611
|
function serializeApprovalCommand (command) {
|
|
634
|
-
if (command.usesShell) return command.shellCommand
|
|
635
612
|
return command.argv.map(formatApprovalArgument).join(' ')
|
|
636
613
|
}
|
|
637
614
|
|
|
@@ -649,52 +626,19 @@ function formatApprovalArgument (value) {
|
|
|
649
626
|
}
|
|
650
627
|
|
|
651
628
|
function serializeDisplayCommand (command) {
|
|
652
|
-
|
|
653
|
-
return command.displayCommand.trim()
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (command.usesShell) return command.shellCommand
|
|
657
|
-
|
|
658
|
-
return getDisplayArgv(command.argv).join(' ')
|
|
629
|
+
return command.argv.join(' ')
|
|
659
630
|
}
|
|
660
631
|
|
|
661
632
|
function getCommandDetails (command) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (!details.exactCommandCollapsed) return
|
|
666
|
-
|
|
667
|
-
return details
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function getDisplayArgv (argv) {
|
|
671
|
-
const { prefixAssignments, commandIndex, corepackIndex } = parseArgv(argv)
|
|
672
|
-
if (corepackIndex !== -1) return [...prefixAssignments, ...argv.slice(corepackIndex + 1)]
|
|
673
|
-
return [...prefixAssignments, ...argv.slice(commandIndex)]
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function getDisplayDetails (argv) {
|
|
677
|
-
const { commandIndex, corepackIndex, pathAdjusted } = parseArgv(argv)
|
|
678
|
-
const displayArgv = getDisplayArgv(argv)
|
|
679
|
-
const details = {
|
|
680
|
-
exactCommandCollapsed: displayArgv.join(' ') !== argv.join(' '),
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if (pathAdjusted) details.pathAdjusted = true
|
|
684
|
-
|
|
685
|
-
if (corepackIndex !== -1) {
|
|
686
|
-
details.runtimeWrapper = 'node/corepack'
|
|
687
|
-
details.packageManager = argv[corepackIndex + 1]
|
|
688
|
-
} else if (commandIndex > 0) {
|
|
689
|
-
details.runtimeWrapper = 'env'
|
|
633
|
+
return {
|
|
634
|
+
executionBoundary: 'validator-owned-direct-runner',
|
|
635
|
+
runner: command.argv[1],
|
|
690
636
|
}
|
|
691
|
-
|
|
692
|
-
return details
|
|
693
637
|
}
|
|
694
638
|
|
|
695
639
|
module.exports = {
|
|
696
640
|
runCommand,
|
|
697
|
-
|
|
641
|
+
buildOfflineCaptureEnv,
|
|
698
642
|
buildDatadogEnv,
|
|
699
643
|
getBaseEnv,
|
|
700
644
|
getCommandDetails,
|