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,780 +1,560 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const path = require('path')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
5
|
|
|
6
6
|
const { buildCiCommandCandidate } = require('../ci-command-candidate')
|
|
7
|
+
const { expandLocalPackageScripts } = require('../ci-package-scripts')
|
|
7
8
|
const { buildCiRemediation } = require('../ci-remediation')
|
|
8
|
-
const { getCommandBlocker } = require('../command-blocker')
|
|
9
|
-
const { serializeCommand } = require('../command-runner')
|
|
10
9
|
const { getFrameworkCiDiscoveryContradiction } = require('../ci-discovery')
|
|
11
|
-
const {
|
|
12
|
-
const {
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pass,
|
|
26
|
-
runInstrumentedCommand,
|
|
27
|
-
tailInterestingLines,
|
|
28
|
-
} = require('./helpers')
|
|
29
|
-
|
|
30
|
-
const INCONCLUSIVE_CI_WIRING_FAILURES = new Set([
|
|
31
|
-
'package-manager-filesystem-blocked',
|
|
32
|
-
'package-manager-version-mismatch',
|
|
33
|
-
'watchman-filesystem-blocked',
|
|
34
|
-
'ci-wiring-command-failed-before-tests',
|
|
35
|
-
'ci-wiring-command-timed-out',
|
|
36
|
-
'ci-wiring-no-observed-tests',
|
|
37
|
-
'ci-wiring-project-filter-mismatch',
|
|
38
|
-
'ci-wiring-test-filter-mismatch',
|
|
39
|
-
'no-test-optimization-events',
|
|
40
|
-
])
|
|
41
|
-
|
|
42
|
-
async function runCiWiring ({ manifest, framework, out, options, basicResult }) {
|
|
43
|
-
const scenarioName = 'ci-wiring'
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const command = getCiWiringCommand(framework)
|
|
47
|
-
if (!command) return getMissingCiWiringCommandResult(framework, manifest)
|
|
48
|
-
|
|
49
|
-
const outDir = frameworkOutDir(out, framework, scenarioName)
|
|
50
|
-
ensureSafeDirectory(out, outDir, 'CI wiring artifact directory')
|
|
51
|
-
const baseEvidence = getCiWiringBaseEvidence({ framework, manifest, basicResult, command })
|
|
52
|
-
const run = await runInstrumentedCommand({
|
|
53
|
-
framework,
|
|
54
|
-
out,
|
|
55
|
-
scenarioName,
|
|
56
|
-
command,
|
|
57
|
-
options,
|
|
58
|
-
ciWiring: true,
|
|
59
|
-
})
|
|
60
|
-
const { result, events } = run
|
|
61
|
-
|
|
62
|
-
const ciWiringPreflight = getComparableCiWiringPreflight(framework, command)
|
|
63
|
-
const evidence = {
|
|
64
|
-
...baseEvidence,
|
|
65
|
-
commandExitCode: result.exitCode,
|
|
66
|
-
commandTimedOut: result.timedOut,
|
|
67
|
-
commandDescription: command.description,
|
|
68
|
-
commandOutputSummary: summarizeTestOutput(result.stdout, result.stderr),
|
|
69
|
-
ciCommandExecution: {
|
|
70
|
-
mode: 'full-replay',
|
|
71
|
-
fullReplayRan: true,
|
|
72
|
-
},
|
|
73
|
-
preflight: summarizePreflight(ciWiringPreflight),
|
|
74
|
-
settingsLoadedFromCache: run.offline.inputs.settings?.status === 'loaded',
|
|
75
|
-
offlineExporterCapture: {
|
|
76
|
-
mode: run.offline.captureMode,
|
|
77
|
-
completionCount: run.offline.completionCount,
|
|
78
|
-
observedEventCount: run.offline.observedEventCount,
|
|
79
|
-
retainedEventCount: run.offline.retainedEventCount,
|
|
80
|
-
sampled: run.offline.sampled,
|
|
81
|
-
},
|
|
82
|
-
offlineExporterSummary: run.offline.summary,
|
|
83
|
-
...basicEventEvidence(events),
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!hasAllBasicEventTypes(events)) {
|
|
87
|
-
const commandFailure = summarizeCiCommandFailure(result, evidence)
|
|
88
|
-
if (commandFailure.kind !== 'ci-wiring-command-result-unknown') {
|
|
89
|
-
evidence.commandFailure = commandFailure
|
|
90
|
-
}
|
|
91
|
-
evidence.debugSignals = summarizeCiDebugSignals(result)
|
|
92
|
-
const probe = await maybeRunInitializationProbe({ command, framework, options, outDir, result, evidence })
|
|
93
|
-
if (probe.summary) evidence.initializationProbe = probe.summary
|
|
94
|
-
evidence.monorepoFindings = getMonorepoFindings({ framework, command, probe: probe.summary })
|
|
95
|
-
evidence.eventLevelFailure = getCiWiringEventFailure({ framework, result, evidence, basicResult })
|
|
96
|
-
if (isInconclusiveCiWiringFailure(evidence.eventLevelFailure)) {
|
|
97
|
-
return inconclusive(
|
|
98
|
-
framework,
|
|
99
|
-
scenarioName,
|
|
100
|
-
evidence.eventLevelFailure.summary,
|
|
101
|
-
evidence,
|
|
102
|
-
outDir,
|
|
103
|
-
probe.artifacts
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
return fail(framework, scenarioName, evidence.eventLevelFailure.summary, evidence, outDir, probe.artifacts)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (result.exitCode === 0) {
|
|
110
|
-
return pass(
|
|
111
|
-
framework,
|
|
112
|
-
scenarioName,
|
|
113
|
-
'The CI test command emitted session, module, suite, and test events with the initialization configured ' +
|
|
114
|
-
'by CI.',
|
|
115
|
-
evidence,
|
|
116
|
-
outDir
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (matchesPreflightExitCode(ciWiringPreflight, result.exitCode)) {
|
|
121
|
-
evidence.commandExitMatchesPreflight = true
|
|
122
|
-
return pass(
|
|
123
|
-
framework,
|
|
124
|
-
scenarioName,
|
|
125
|
-
'The CI test command emitted session, module, suite, and test events with the initialization configured ' +
|
|
126
|
-
'by CI. ' +
|
|
127
|
-
`The command exited ${result.exitCode}, matching the dd-trace-less preflight run.`,
|
|
128
|
-
evidence,
|
|
129
|
-
outDir
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
evidence.commandExitMatchesPreflight = false
|
|
134
|
-
return fail(
|
|
135
|
-
framework,
|
|
136
|
-
scenarioName,
|
|
137
|
-
`CI wiring emitted Test Optimization events, but the command exited ${result.exitCode}.`,
|
|
138
|
-
evidence,
|
|
139
|
-
outDir
|
|
140
|
-
)
|
|
141
|
-
} catch (err) {
|
|
142
|
-
return error(framework, scenarioName, err)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isInconclusiveCiWiringFailure (failure) {
|
|
147
|
-
return INCONCLUSIVE_CI_WIRING_FAILURES.has(failure.kind)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getCiWiringBaseEvidence ({ framework, manifest, basicResult, command }) {
|
|
151
|
-
return {
|
|
152
|
-
commandDescription: command.description,
|
|
153
|
-
ciCommandCandidate: buildCiCommandCandidate(framework),
|
|
154
|
-
ciWiring: framework.ciWiring,
|
|
155
|
-
ciRemediation: buildCiRemediation(framework),
|
|
156
|
-
nodeOptionsRemoval: findNodeOptionsRemoval(framework, manifest),
|
|
157
|
-
existingDatadogInitScripts: findDatadogInitScripts(manifest, framework),
|
|
158
|
-
lateInitialization: findLateInitialization(manifest, framework),
|
|
159
|
-
directInitializationBasicReporting: summarizeBasicReportingResult(basicResult),
|
|
160
|
-
}
|
|
10
|
+
const { environmentNamesEqual } = require('../environment')
|
|
11
|
+
const { parseLiteralEnvironmentPrefix } = require('../literal-environment')
|
|
12
|
+
const { fail, incomplete } = require('./helpers')
|
|
13
|
+
|
|
14
|
+
const MAX_CI_FILE_BYTES = 512 * 1024
|
|
15
|
+
const DYNAMIC_COMMAND_PATTERN = /[$`;&|\r\n]|%[^%\s]+%|![^!\s]+!/
|
|
16
|
+
const RUNNER_PATTERNS = {
|
|
17
|
+
cucumber:
|
|
18
|
+
/^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?cucumber(?:-js)?(?:\.js)?(?:\s|$)/,
|
|
19
|
+
cypress: /^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?cypress(?:\.js)?\s+run(?:\s|$)/,
|
|
20
|
+
jest: /^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?jest(?:\.js)?(?:\s|$)/,
|
|
21
|
+
mocha: /^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?mocha(?:\.js)?(?:\s|$)/,
|
|
22
|
+
playwright: /^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?playwright(?:\.js)?\s+test(?:\s|$)/,
|
|
23
|
+
vitest: /^(?:(?:[^\s]*[/\\])?node(?:\.exe)?\s+)?(?:[^\s]*[/\\])?vitest(?:\.js)?\s+(?:run|--run)(?:\s|$)/,
|
|
161
24
|
}
|
|
162
25
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
reason: `The package script expansion ${evidence.nodeOptionsRemoval.command} explicitly removes ` +
|
|
173
|
-
'NODE_OPTIONS before the test runner starts.',
|
|
174
|
-
},
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
return await runInitializationProbe({
|
|
180
|
-
command,
|
|
181
|
-
framework,
|
|
182
|
-
options,
|
|
183
|
-
outDir,
|
|
184
|
-
})
|
|
185
|
-
} catch (err) {
|
|
186
|
-
return {
|
|
187
|
-
summary: {
|
|
188
|
-
ran: false,
|
|
189
|
-
error: err && err.message ? err.message : String(err),
|
|
190
|
-
},
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function getMissingCiWiringCommandResult (framework, manifest) {
|
|
26
|
+
/**
|
|
27
|
+
* Audits only structurally anchored, literal CI evidence.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} input audit inputs
|
|
30
|
+
* @param {object} input.manifest validation manifest
|
|
31
|
+
* @param {object} input.framework framework entry
|
|
32
|
+
* @returns {object} CI audit result
|
|
33
|
+
*/
|
|
34
|
+
function runCiWiring ({ manifest, framework }) {
|
|
196
35
|
const contradiction = getFrameworkCiDiscoveryContradiction(framework, manifest)
|
|
197
36
|
if (contradiction) {
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
ciDiscovery: contradiction.ciDiscovery,
|
|
203
|
-
recommendation: contradiction.recommendation,
|
|
204
|
-
})
|
|
37
|
+
return getIncomplete(framework, contradiction.reason, {
|
|
38
|
+
ciDiscovery: contradiction.ciDiscovery,
|
|
39
|
+
recommendation: contradiction.recommendation,
|
|
40
|
+
})
|
|
205
41
|
}
|
|
206
42
|
|
|
207
|
-
const
|
|
208
|
-
const diagnosis = ciWiring?.replayBlocker ||
|
|
209
|
-
ciWiring?.diagnosis ||
|
|
210
|
-
ciWiring?.reason ||
|
|
211
|
-
'No replayable CI wiring command was provided in the manifest.'
|
|
212
|
-
const ciRemediation = buildCiRemediation(framework)
|
|
43
|
+
const ci = framework.ciWiring || {}
|
|
213
44
|
const evidence = {
|
|
214
45
|
ciCommandCandidate: buildCiCommandCandidate(framework),
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
46
|
+
conclusion: 'incomplete',
|
|
47
|
+
domain: 'ci_configuration',
|
|
48
|
+
evidenceStrength: 'unknown',
|
|
218
49
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
...localFailure,
|
|
235
|
-
kind: 'ci-wiring-no-test-optimization-events',
|
|
236
|
-
summary: getCiWiringTestsRanSummary({ basicResult, evidence, framework }),
|
|
237
|
-
recommendation: getCiWiringTestsRanRecommendation({ basicResult, evidence, framework }),
|
|
238
|
-
}
|
|
50
|
+
const missing = getMissingReviewFields(ci)
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
return getIncomplete(
|
|
53
|
+
framework,
|
|
54
|
+
`The CI audit is incomplete because ${[
|
|
55
|
+
`it is missing ${missing.join(', ')}`,
|
|
56
|
+
...(ci.reviewComplete === true ? [] : ['the review is not marked complete']),
|
|
57
|
+
].join(' and ')}.`,
|
|
58
|
+
{
|
|
59
|
+
...evidence,
|
|
60
|
+
recommendation: 'Identify one exact CI test job and resolve inherited configuration and wrappers. Leave the ' +
|
|
61
|
+
'result incomplete when that cannot be proven statically.',
|
|
62
|
+
}
|
|
63
|
+
)
|
|
239
64
|
}
|
|
240
65
|
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
return {
|
|
244
|
-
...
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
signals: commandFailure.signals,
|
|
248
|
-
summary: commandFailure.summary,
|
|
249
|
-
recommendation: commandFailure.recommendation,
|
|
250
|
-
}
|
|
66
|
+
const source = readCiSource(ci.configFile)
|
|
67
|
+
if (!source) {
|
|
68
|
+
return getIncomplete(framework, 'The recorded CI configuration file is unavailable or too large to verify.', {
|
|
69
|
+
...evidence,
|
|
70
|
+
recommendation: 'Regenerate the manifest from the current checkout and review the CI file again.',
|
|
71
|
+
})
|
|
251
72
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
'
|
|
73
|
+
const jobSource = getSelectedJobSource(source, ci)
|
|
74
|
+
if (!jobSource ||
|
|
75
|
+
!containsExecutionCommand(jobSource, ci.command) ||
|
|
76
|
+
(ci.step && !containsLiteral(jobSource, ci.step))) {
|
|
77
|
+
return getIncomplete(
|
|
78
|
+
framework,
|
|
79
|
+
'The recorded command and step could not be bound structurally to the selected job in the checksum-bound ' +
|
|
80
|
+
'CI file.',
|
|
81
|
+
{
|
|
82
|
+
...evidence,
|
|
83
|
+
recommendation: 'Record the exact YAML job key, literal step, and command from one supported CI job. Leave ' +
|
|
84
|
+
'the audit incomplete when that job structure cannot be verified.',
|
|
85
|
+
}
|
|
86
|
+
)
|
|
259
87
|
}
|
|
260
|
-
}
|
|
261
88
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
89
|
+
const command = ci.command.trim()
|
|
90
|
+
const resolution = getRunnerResolution(command, framework, ci)
|
|
91
|
+
const normalizedSource = jobSource.replaceAll('\\', '/')
|
|
92
|
+
const hasInitialization = /dd-trace\/ci\/init(?:\.js)?\b/.test(normalizedSource)
|
|
93
|
+
const matrixRelevant = matrixAffectsCiFacts(jobSource, command)
|
|
94
|
+
const unresolved = classifyUnresolved(ci, resolution, matrixRelevant, jobSource)
|
|
95
|
+
const initialization = getInitializationFact(ci, hasInitialization)
|
|
96
|
+
const ciFacts = {
|
|
97
|
+
initialization,
|
|
98
|
+
matrix: {
|
|
99
|
+
status: matrixRelevant ? 'affects_relevant_configuration' : 'not_relevant_to_ci_facts',
|
|
100
|
+
},
|
|
101
|
+
runnerInvocation: {
|
|
102
|
+
...(resolution.commandPath ? { commandPath: resolution.commandPath } : {}),
|
|
103
|
+
...(resolution.lifecycleScripts?.length > 0
|
|
104
|
+
? { lifecycleScripts: resolution.lifecycleScripts }
|
|
105
|
+
: {}),
|
|
106
|
+
...(resolution.reason ? { reason: resolution.reason } : {}),
|
|
107
|
+
...(resolution.resolvedCommand ? { resolvedCommand: resolution.resolvedCommand } : {}),
|
|
108
|
+
source: resolution.source,
|
|
109
|
+
status: resolution.status,
|
|
110
|
+
},
|
|
111
|
+
transport: getTransportFact(ci, jobSource),
|
|
112
|
+
unresolved,
|
|
113
|
+
}
|
|
114
|
+
evidence.ciFacts = ciFacts
|
|
115
|
+
|
|
116
|
+
const effectiveNodeOptions = resolution.status === 'confirmed'
|
|
117
|
+
? getEffectiveNodeOptionsOverride(resolution.commandPath)
|
|
118
|
+
: undefined
|
|
119
|
+
const remediation = buildCiRemediation(framework)
|
|
120
|
+
if (resolution.status === 'confirmed' &&
|
|
121
|
+
effectiveNodeOptions !== undefined &&
|
|
122
|
+
!/dd-trace[\\/]ci[\\/]init(?:\.js)?\b/.test(effectiveNodeOptions) &&
|
|
123
|
+
unresolved.relevant.length === 0) {
|
|
124
|
+
return getFailure(
|
|
125
|
+
framework,
|
|
126
|
+
'The statically resolved CI test path overrides NODE_OPTIONS without the dd-trace/ci/init preload, so Test ' +
|
|
127
|
+
'Optimization is not initialized in the final runner.',
|
|
128
|
+
{
|
|
129
|
+
...evidence,
|
|
130
|
+
ciRemediation: remediation,
|
|
131
|
+
conclusion: 'confirmed_misconfigured',
|
|
132
|
+
evidenceStrength: 'confirmed_static',
|
|
133
|
+
recommendation: remediation.summary,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
265
136
|
}
|
|
266
137
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
138
|
+
if (initialization.status === 'missing' &&
|
|
139
|
+
resolution.status === 'confirmed' &&
|
|
140
|
+
unresolved.relevant.length === 0) {
|
|
141
|
+
return getFailure(
|
|
142
|
+
framework,
|
|
143
|
+
'The checksum-bound CI job reaches the selected test framework through a bounded static command path, but ' +
|
|
144
|
+
'does not configure the dd-trace/ci/init preload. Test Optimization is not initialized in that job.',
|
|
145
|
+
{
|
|
146
|
+
...evidence,
|
|
147
|
+
ciRemediation: remediation,
|
|
148
|
+
conclusion: 'confirmed_misconfigured',
|
|
149
|
+
evidenceStrength: 'confirmed_static',
|
|
150
|
+
recommendation: remediation.summary,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
279
153
|
}
|
|
280
154
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
: 'The package script'
|
|
296
|
-
recommendation = `${source} clears NODE_OPTIONS before the test runner starts. Remove the empty ` +
|
|
297
|
-
'`NODE_OPTIONS=` assignment, or pass the CI-provided `-r dd-trace/ci/init` preload to the next command.'
|
|
298
|
-
} else if (lateInitialization.length > 0) {
|
|
299
|
-
const setupFiles = lateInitialization.map(finding => `\`${finding.setupFile}\``).join(', ')
|
|
300
|
-
recommendation = `Move Test Optimization initialization out of Vitest setup file ${setupFiles}. ` +
|
|
301
|
-
'Vitest setup files run after the test runner starts, which is too late for dd-trace to instrument the ' +
|
|
302
|
-
'runner. Set `NODE_OPTIONS=-r dd-trace/ci/init` on the CI test command instead.'
|
|
303
|
-
} else if (existingInitScripts.length > 0) {
|
|
304
|
-
const scriptNames = existingInitScripts.map(script => `\`${script.name}\``).join(', ')
|
|
305
|
-
recommendation = `The package already defines ${scriptNames} with the required ` +
|
|
306
|
-
'`dd-trace/ci/init` preload. Update the identified CI test step to invoke that script, or copy its ' +
|
|
307
|
-
'`NODE_OPTIONS` initialization into the CI test command.'
|
|
308
|
-
} else if (probeReachedTestRunner) {
|
|
309
|
-
recommendation = `${evidence.ciRemediation?.summary || buildCiRemediation(framework).summary} ` +
|
|
310
|
-
'The NODE_OPTIONS probe reached the test runner for this command shape, so no package-manager or wrapper ' +
|
|
311
|
-
'change is needed.'
|
|
312
|
-
} else {
|
|
313
|
-
recommendation = 'Verify that the CI workflow sets NODE_OPTIONS with dd-trace/ci/init for the final test ' +
|
|
314
|
-
'runner, and that any package manager, monorepo runner, or wrapper preserves it.'
|
|
155
|
+
if (resolution.status !== 'confirmed') {
|
|
156
|
+
const partial = initialization.status === 'missing'
|
|
157
|
+
? 'The selected job has no visible dd-trace/ci/init preload. '
|
|
158
|
+
: ''
|
|
159
|
+
return getIncomplete(
|
|
160
|
+
framework,
|
|
161
|
+
`${partial}The CI audit remains incomplete because the selected command could not be resolved to the ` +
|
|
162
|
+
`${framework.framework} runner: ${resolution.reason}.`,
|
|
163
|
+
{
|
|
164
|
+
...evidence,
|
|
165
|
+
recommendation: 'Resolve the remaining wrapper or dynamic command statically, or confirm the effective ' +
|
|
166
|
+
'NODE_OPTIONS value in the final test process with DD_TRACE_DEBUG=1.',
|
|
167
|
+
}
|
|
168
|
+
)
|
|
315
169
|
}
|
|
316
170
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'
|
|
171
|
+
if (unresolved.relevant.length > 0) {
|
|
172
|
+
return getIncomplete(
|
|
173
|
+
framework,
|
|
174
|
+
'The framework invocation was resolved statically, but relevant CI evidence remains unresolved: ' +
|
|
175
|
+
`${unresolved.relevant.join('; ')}.`,
|
|
176
|
+
{
|
|
177
|
+
...evidence,
|
|
178
|
+
recommendation: 'Resolve only the listed configuration that can affect initialization, runner invocation, ' +
|
|
179
|
+
'or transport. Unrelated runtime matrices do not need further analysis.',
|
|
180
|
+
}
|
|
181
|
+
)
|
|
321
182
|
}
|
|
322
183
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: 'In the selected CI test job, '
|
|
332
|
-
const source = finding.scriptName && finding.packageJson
|
|
333
|
-
? `script \`${finding.scriptName}\` in \`${finding.packageJson}\``
|
|
334
|
-
: 'a package script'
|
|
335
|
-
const directResult = basicResult?.status === 'pass'
|
|
336
|
-
? ` When the same ${frameworkName} test command runs with ` +
|
|
337
|
-
'`NODE_OPTIONS=-r dd-trace/ci/init` supplied directly, it reports test data successfully.'
|
|
338
|
-
: ''
|
|
339
|
-
|
|
340
|
-
return 'The CI test command ran tests, but no Test Optimization events reached the offline event artifact. ' +
|
|
341
|
-
`${ciCommand}` +
|
|
342
|
-
`${source} expands to \`${finding.command}\`. The empty \`NODE_OPTIONS=\` assignment clears the Datadog ` +
|
|
343
|
-
`preload before ${frameworkName} starts.${directResult}`
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function findNodeOptionsRemoval (framework, manifest) {
|
|
347
|
-
const commands = framework.ciWiring?.packageScriptExpansionChain || []
|
|
348
|
-
for (const command of commands) {
|
|
349
|
-
if (typeof command !== 'string') continue
|
|
350
|
-
if (/(?:^|\s)NODE_OPTIONS\s*=\s*(?=\s|$)/.test(command) ||
|
|
351
|
-
/(?:^|\s)unset\s+NODE_OPTIONS(?:\s|$)/.test(command) ||
|
|
352
|
-
/(?:^|\s)env\s+-u\s+NODE_OPTIONS(?:\s|$)/.test(command)) {
|
|
353
|
-
return {
|
|
354
|
-
command,
|
|
355
|
-
...findPackageScriptSource(manifest, framework, command),
|
|
184
|
+
if (initialization.status !== 'configured') {
|
|
185
|
+
return getIncomplete(
|
|
186
|
+
framework,
|
|
187
|
+
'Static evidence does not establish whether dd-trace/ci/init is configured for the selected test path.',
|
|
188
|
+
{
|
|
189
|
+
...evidence,
|
|
190
|
+
recommendation: 'Record the literal NODE_OPTIONS configuration for this job or rerun the CI step with ' +
|
|
191
|
+
'DD_TRACE_DEBUG=1.',
|
|
356
192
|
}
|
|
357
|
-
|
|
193
|
+
)
|
|
358
194
|
}
|
|
359
|
-
}
|
|
360
195
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
196
|
+
if (ci.transport?.mode === 'agentless' &&
|
|
197
|
+
/DD_CIVISIBILITY_AGENTLESS_ENABLED/.test(jobSource) &&
|
|
198
|
+
!/\b(?:DD_API_KEY|DATADOG_API_KEY)\b/.test(jobSource)) {
|
|
199
|
+
return getIncomplete(
|
|
200
|
+
framework,
|
|
201
|
+
'The selected job visibly enables agentless reporting but has no Datadog API key reference in the ' +
|
|
202
|
+
'checksum-bound CI file. The key may still be injected outside this file.',
|
|
203
|
+
{
|
|
204
|
+
...evidence,
|
|
205
|
+
ciRemediation: remediation,
|
|
206
|
+
recommendation: 'Confirm that DD_API_KEY reaches this test job from the CI secret store.',
|
|
207
|
+
}
|
|
208
|
+
)
|
|
375
209
|
}
|
|
376
|
-
return {}
|
|
377
|
-
}
|
|
378
210
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
211
|
+
return getIncomplete(
|
|
212
|
+
framework,
|
|
213
|
+
'The selected CI job contains Test Optimization initialization, but static inspection cannot prove that the ' +
|
|
214
|
+
'effective environment and reporting transport reach the final process at runtime.',
|
|
215
|
+
{
|
|
216
|
+
...evidence,
|
|
217
|
+
conclusion: 'configured_propagation_unverified',
|
|
218
|
+
evidenceStrength: 'inferred_static',
|
|
219
|
+
recommendation: 'Rerun this exact CI step with DD_TRACE_DEBUG=1 and confirm initialization in the final test ' +
|
|
220
|
+
'runner.',
|
|
221
|
+
}
|
|
222
|
+
)
|
|
385
223
|
}
|
|
386
224
|
|
|
387
225
|
/**
|
|
388
|
-
*
|
|
226
|
+
* Returns the final inline NODE_OPTIONS override along the resolved command path.
|
|
389
227
|
*
|
|
390
|
-
* @param {
|
|
391
|
-
* @
|
|
392
|
-
* @returns {{name: string, packageJson: string}[]} matching package scripts
|
|
228
|
+
* @param {string[]} commandPath selected command and expanded package scripts
|
|
229
|
+
* @returns {string|undefined} final literal override
|
|
393
230
|
*/
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
let packageJson
|
|
400
|
-
try {
|
|
401
|
-
packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
402
|
-
} catch {
|
|
403
|
-
continue
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
for (const [name, command] of Object.entries(packageJson.scripts || {})) {
|
|
407
|
-
if (typeof command !== 'string' || !/\bNODE_OPTIONS\s*=.*\bdd-trace\/ci\/init\b/.test(command)) {
|
|
408
|
-
continue
|
|
409
|
-
}
|
|
410
|
-
scripts.push({
|
|
411
|
-
name,
|
|
412
|
-
packageJson: path.join(root, 'package.json'),
|
|
413
|
-
})
|
|
231
|
+
function getEffectiveNodeOptionsOverride (commandPath = []) {
|
|
232
|
+
let value
|
|
233
|
+
for (const command of commandPath) {
|
|
234
|
+
for (const assignment of normalizeDirectCommand(command).assignments) {
|
|
235
|
+
if (environmentNamesEqual(assignment.name, 'NODE_OPTIONS')) value = assignment.value
|
|
414
236
|
}
|
|
415
237
|
}
|
|
416
|
-
|
|
417
|
-
return scripts
|
|
238
|
+
return value
|
|
418
239
|
}
|
|
419
240
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (
|
|
241
|
+
/**
|
|
242
|
+
* Resolves a direct runner or a bounded chain of local package scripts.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} command selected CI command
|
|
245
|
+
* @param {object} framework framework manifest entry
|
|
246
|
+
* @param {object} ci selected CI evidence
|
|
247
|
+
* @returns {object} static runner resolution
|
|
248
|
+
*/
|
|
249
|
+
function getRunnerResolution (command, framework, ci) {
|
|
250
|
+
if (getDirectRunner(command, framework.framework)) {
|
|
430
251
|
return {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
recommendation: 'Review CI wiring event failure evidence.',
|
|
252
|
+
commandPath: [command],
|
|
253
|
+
resolvedCommand: command,
|
|
254
|
+
source: 'direct_ci_command',
|
|
255
|
+
status: 'confirmed',
|
|
436
256
|
}
|
|
437
257
|
}
|
|
438
258
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const preloadFailure = detectDatadogPreloadResolutionFailure(output)
|
|
443
|
-
if (preloadFailure) {
|
|
259
|
+
if (!ci.workingDirectory ||
|
|
260
|
+
path.resolve(ci.workingDirectory) !== path.resolve(framework.project.root)) {
|
|
444
261
|
return {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
recommendation: 'Make sure `dd-trace` is installed where the CI command starts, or run the CI command from ' +
|
|
451
|
-
'the package working directory that can resolve `dd-trace/ci/init`. After the preload resolves, rerun CI ' +
|
|
452
|
-
'wiring validation to check whether the required Datadog setup reaches the final test runner.',
|
|
262
|
+
reason: ci.workingDirectory
|
|
263
|
+
? 'the selected working directory does not match the approval-bound project package'
|
|
264
|
+
: 'the selected package-script command has no approval-bound effective working directory',
|
|
265
|
+
source: 'unresolved_wrapper',
|
|
266
|
+
status: 'unresolved',
|
|
453
267
|
}
|
|
454
268
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
269
|
+
const scripts = readProjectScripts(framework.project.packageJson)
|
|
270
|
+
if (!scripts) {
|
|
457
271
|
return {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
summary: 'The CI-shaped command timed out before the validator could observe Test Optimization events.',
|
|
462
|
-
recommendation: 'Choose a smaller representative CI test command or record the setup needed to make the ' +
|
|
463
|
-
'selected command complete within the validation timeout.',
|
|
272
|
+
reason: 'the approval-bound project package.json is unavailable or invalid',
|
|
273
|
+
source: 'unresolved_wrapper',
|
|
274
|
+
status: 'unresolved',
|
|
464
275
|
}
|
|
465
276
|
}
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
const termination = Number.isInteger(result.exitCode) ? `exited ${result.exitCode}` : 'failed'
|
|
469
|
-
const projectFilterMismatch = output.match(/No projects matched the filter\s+["']([^"']+)["']/i)
|
|
470
|
-
if (projectFilterMismatch) {
|
|
471
|
-
return {
|
|
472
|
-
...common,
|
|
473
|
-
kind: 'ci-wiring-project-filter-mismatch',
|
|
474
|
-
signals: [projectFilterMismatch[0]],
|
|
475
|
-
summary: `The CI-shaped command ${termination} before tests because its added project filter ` +
|
|
476
|
-
`\`${projectFilterMismatch[1]}\` is not exposed by the configuration loaded from the CI working ` +
|
|
477
|
-
'directory. No CI wiring conclusion was reached.',
|
|
478
|
-
recommendation: 'Remove the invented project selector. Choose a representative test from a project the ' +
|
|
479
|
-
'original CI command actually loads, or mark CI replay unavailable when the real CI wrapper cannot be ' +
|
|
480
|
-
'focused safely.',
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (/No test files found/i.test(output)) {
|
|
485
|
-
return {
|
|
486
|
-
...common,
|
|
487
|
-
kind: 'ci-wiring-test-filter-mismatch',
|
|
488
|
-
signals: findInterestingLines(output, [/No test files found/, /filter:/, /include:/]),
|
|
489
|
-
summary: `The CI-shaped command ${termination} before tests because its focused test filter matched no ` +
|
|
490
|
-
'files in the project configuration loaded by CI. No CI wiring conclusion was reached.',
|
|
491
|
-
recommendation: 'Choose a real test included by the exact CI-loaded project and use it consistently for ' +
|
|
492
|
-
'Basic Reporting and CI wiring. If the CI command cannot be focused without changing its project, cwd, ' +
|
|
493
|
-
'or wrapper chain, mark CI replay unavailable.',
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const buildErrors = findInterestingLines(output, [
|
|
498
|
-
/Cannot find module/,
|
|
499
|
-
/Module not found/,
|
|
500
|
-
/Error \[ERR_MODULE_NOT_FOUND\]/,
|
|
501
|
-
/Could not resolve /,
|
|
502
|
-
/command not found/,
|
|
503
|
-
])
|
|
504
|
-
|
|
277
|
+
const expansion = expandLocalPackageScripts(command, scripts)
|
|
278
|
+
if (expansion.error) {
|
|
505
279
|
return {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
summary: `The CI-shaped command ${termination} before the validator observed any tests running. ` +
|
|
511
|
-
'No CI wiring conclusion about Test Optimization initialization was reached for this command.',
|
|
512
|
-
recommendation: 'Fix or document the command/setup failure first. CI wiring can only be interpreted after ' +
|
|
513
|
-
'the selected CI-shaped command reaches the test runner.',
|
|
280
|
+
lifecycleScripts: expansion.lifecycleScripts,
|
|
281
|
+
reason: expansion.error,
|
|
282
|
+
source: 'local_package_script',
|
|
283
|
+
status: 'unresolved',
|
|
514
284
|
}
|
|
515
285
|
}
|
|
516
|
-
|
|
517
|
-
|
|
286
|
+
const candidate = expansion.terminals.find(terminal => {
|
|
287
|
+
return getDirectRunner(terminal.command, framework.framework)
|
|
288
|
+
})
|
|
289
|
+
if (!candidate) {
|
|
518
290
|
return {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
'Optimization events.',
|
|
524
|
-
recommendation: 'Verify that the selected CI step actually runs tests. If it is a wrapper with unusual ' +
|
|
525
|
-
'output, record preflight observedTestCount or choose a representative command whose output identifies ' +
|
|
526
|
-
'the test result.',
|
|
291
|
+
lifecycleScripts: expansion.lifecycleScripts,
|
|
292
|
+
reason: 'no bounded local package-script path reaches the selected framework runner',
|
|
293
|
+
source: 'unresolved_wrapper',
|
|
294
|
+
status: 'unresolved',
|
|
527
295
|
}
|
|
528
296
|
}
|
|
529
|
-
|
|
530
297
|
return {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
298
|
+
commandPath: candidate.path,
|
|
299
|
+
lifecycleScripts: expansion.lifecycleScripts,
|
|
300
|
+
resolvedCommand: candidate.command,
|
|
301
|
+
source: 'local_package_script',
|
|
302
|
+
status: 'confirmed',
|
|
536
303
|
}
|
|
537
304
|
}
|
|
538
305
|
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
306
|
+
function readProjectScripts (filename) {
|
|
307
|
+
try {
|
|
308
|
+
const stat = fs.lstatSync(filename)
|
|
309
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_CI_FILE_BYTES) return
|
|
310
|
+
const packageJson = JSON.parse(fs.readFileSync(filename, 'utf8'))
|
|
311
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== 'object' ||
|
|
312
|
+
Array.isArray(packageJson.scripts)) return {}
|
|
313
|
+
return packageJson.scripts
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
546
316
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
317
|
+
function getInitializationFact (ci, hasInitialization) {
|
|
318
|
+
if (ci.initialization?.status === 'configured' && hasInitialization) {
|
|
319
|
+
return { status: 'configured', evidence: ci.initialization.evidence }
|
|
320
|
+
}
|
|
321
|
+
if (ci.initialization?.status === 'not_configured' && !hasInitialization) {
|
|
322
|
+
return { status: 'missing', evidence: ci.initialization.evidence }
|
|
552
323
|
}
|
|
553
|
-
|
|
554
324
|
return {
|
|
555
|
-
|
|
556
|
-
reason: '
|
|
325
|
+
status: 'unresolved',
|
|
326
|
+
reason: 'recorded initialization status and checksum-bound CI source do not establish the same conclusion',
|
|
557
327
|
}
|
|
558
328
|
}
|
|
559
329
|
|
|
560
|
-
function
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function detectDatadogPreloadResolutionFailure (output) {
|
|
568
|
-
if (!/dd-trace(?:\/ci\/init)?/.test(output)) return null
|
|
569
|
-
if (!/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND|Cannot find module|Cannot find package/.test(output)) return null
|
|
570
|
-
|
|
571
|
-
const signals = findInterestingLines(output, [
|
|
572
|
-
/Cannot find module ['"]dd-trace\/ci\/init['"]/,
|
|
573
|
-
/Cannot find package ['"]dd-trace['"]/,
|
|
574
|
-
/Error \[ERR_MODULE_NOT_FOUND\].*dd-trace\/ci\/init/,
|
|
575
|
-
/MODULE_NOT_FOUND/,
|
|
576
|
-
/internal\/preload/,
|
|
577
|
-
], 8)
|
|
578
|
-
|
|
579
|
-
return { signals }
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function summarizeCiDebugSignals (result) {
|
|
583
|
-
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
584
|
-
const lines = findInterestingLines(output, [
|
|
585
|
-
/dd-trace/i,
|
|
586
|
-
/datadog/i,
|
|
587
|
-
/ci visibility/i,
|
|
588
|
-
/test optimization/i,
|
|
589
|
-
/ECONNREFUSED/,
|
|
590
|
-
/ECONNRESET/,
|
|
591
|
-
/ETIMEDOUT/,
|
|
592
|
-
/socket hang up/,
|
|
593
|
-
/failed to send/i,
|
|
594
|
-
/writer/i,
|
|
595
|
-
], 12)
|
|
596
|
-
|
|
597
|
-
return {
|
|
598
|
-
debugEnvEnabled: true,
|
|
599
|
-
lines,
|
|
330
|
+
function getTransportFact (ci, jobSource) {
|
|
331
|
+
const mode = ci.transport?.mode
|
|
332
|
+
if (mode === 'agentless' &&
|
|
333
|
+
/DD_CIVISIBILITY_AGENTLESS_ENABLED/.test(jobSource) &&
|
|
334
|
+
!/\b(?:DD_API_KEY|DATADOG_API_KEY)\b/.test(jobSource)) {
|
|
335
|
+
return { status: 'credentials_unverified', mode }
|
|
600
336
|
}
|
|
337
|
+
if (mode === 'agent' || mode === 'agentless') {
|
|
338
|
+
return { status: 'configured', mode, evidence: ci.transport.evidence }
|
|
339
|
+
}
|
|
340
|
+
if (mode === 'none') return { status: 'missing', mode }
|
|
341
|
+
return { status: 'unresolved', mode: mode || 'unknown' }
|
|
601
342
|
}
|
|
602
343
|
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
344
|
+
function classifyUnresolved (ci, resolution, matrixRelevant, jobSource) {
|
|
345
|
+
const relevant = []
|
|
346
|
+
const ignored = []
|
|
347
|
+
const unresolved = Array.isArray(ci.unresolved) ? ci.unresolved : []
|
|
348
|
+
const githubHosted = /[/\\]\.github[/\\]workflows[/\\]/.test(ci.configFile) &&
|
|
349
|
+
/^\s*runs-on:\s*(?:ubuntu|windows|macos)-/mi.test(jobSource)
|
|
350
|
+
|
|
351
|
+
for (const item of unresolved) {
|
|
352
|
+
const isMatrix = /\bmatrix\b/i.test(item)
|
|
353
|
+
const isResolvedPackagePath = resolution.source === 'local_package_script' &&
|
|
354
|
+
/\b(?:lifecycle|npm|package script|pnpm|pretest|posttest|yarn)\b/i.test(item)
|
|
355
|
+
const isAmbientGithubSettings = githubHosted &&
|
|
356
|
+
/\b(?:repository|organization|environment)[-\s,\w]*\b(?:secrets?|variables?)\b/i.test(item) &&
|
|
357
|
+
/\b(?:inject|inherit|outside)\b/i.test(item)
|
|
358
|
+
const isOtherJob = /^Other jobs?\b/i.test(item) ||
|
|
359
|
+
/\bsecond\b.*\bjob\b.*\bonly\b.*\bselected\b/i.test(item) ||
|
|
360
|
+
/\brelease\b.*\bcontains no test job\b/i.test(item)
|
|
361
|
+
if ((isMatrix && !matrixRelevant) ||
|
|
362
|
+
isResolvedPackagePath ||
|
|
363
|
+
isAmbientGithubSettings ||
|
|
364
|
+
isOtherJob) {
|
|
365
|
+
ignored.push(item)
|
|
366
|
+
} else {
|
|
367
|
+
relevant.push(item)
|
|
608
368
|
}
|
|
609
369
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
ran: true,
|
|
613
|
-
status: basicResult.status,
|
|
614
|
-
diagnosis: basicResult.diagnosis,
|
|
370
|
+
if (ci.reviewComplete !== true && unresolved.length === 0) {
|
|
371
|
+
relevant.push('the CI evidence review is not marked complete')
|
|
615
372
|
}
|
|
373
|
+
return { ignored, relevant }
|
|
616
374
|
}
|
|
617
375
|
|
|
618
|
-
function
|
|
619
|
-
|
|
376
|
+
function matrixAffectsCiFacts (jobSource, command) {
|
|
377
|
+
const matrixReference = /\bmatrix\s*[.[]/
|
|
378
|
+
if (!matrixReference.test(jobSource)) return false
|
|
379
|
+
if (matrixReference.test(command)) return true
|
|
380
|
+
return jobSource.split(/\r?\n/).some(line => {
|
|
381
|
+
return matrixReference.test(line) &&
|
|
382
|
+
/\b(?:container|DD_[A-Z0-9_]+|DATADOG_[A-Z0-9_]+|NODE_OPTIONS|runs-on|shell|working-directory)\b/i.test(line)
|
|
383
|
+
})
|
|
384
|
+
}
|
|
620
385
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Locates a direct runner only at the executable position after literal environment assignments.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} command selected CI command
|
|
390
|
+
* @param {string} framework framework name
|
|
391
|
+
* @returns {{index: number}|undefined} direct runner location
|
|
392
|
+
*/
|
|
393
|
+
function getDirectRunner (command, framework) {
|
|
394
|
+
if (DYNAMIC_COMMAND_PATTERN.test(command)) return
|
|
395
|
+
const { index, source } = normalizeDirectCommand(command)
|
|
396
|
+
if (!RUNNER_PATTERNS[framework]?.test(source)) return
|
|
397
|
+
return { index }
|
|
398
|
+
}
|
|
625
399
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
400
|
+
function normalizeDirectCommand (command) {
|
|
401
|
+
const prefix = parseLiteralEnvironmentPrefix(command)
|
|
402
|
+
const assignments = [...prefix.assignments]
|
|
403
|
+
let source = String(command).slice(prefix.length).replace(/^(?:c8|nyc)(?:\.cmd)?\s+/, '')
|
|
404
|
+
if (/^cross-env(?:\.cmd)?\s+/.test(source)) {
|
|
405
|
+
source = source.replace(/^cross-env(?:\.cmd)?\s+/, '')
|
|
406
|
+
const crossEnv = parseLiteralEnvironmentPrefix(source)
|
|
407
|
+
assignments.push(...crossEnv.assignments)
|
|
408
|
+
source = source.slice(crossEnv.length)
|
|
409
|
+
}
|
|
410
|
+
return { assignments, index: prefix.length, source }
|
|
411
|
+
}
|
|
631
412
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Returns the selected YAML job block, or undefined when structural binding is unsupported.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} source CI configuration source
|
|
417
|
+
* @param {object} ci selected CI evidence
|
|
418
|
+
* @returns {string|undefined} selected job source
|
|
419
|
+
*/
|
|
420
|
+
function getSelectedJobSource (source, ci) {
|
|
421
|
+
if (!/\.ya?ml$/i.test(String(ci.configFile || ''))) return
|
|
422
|
+
const lines = source.replaceAll('\r\n', '\n').split('\n')
|
|
423
|
+
const jobName = normalizeYamlKey(ci.job)
|
|
424
|
+
if (!jobName) return
|
|
425
|
+
|
|
426
|
+
const jobsIndex = lines.findIndex(line => /^jobs:\s*(?:#.*)?$/.test(line))
|
|
427
|
+
if (jobsIndex !== -1) {
|
|
428
|
+
const jobsEnd = findYamlBlockEnd(lines, jobsIndex, 0)
|
|
429
|
+
const entries = []
|
|
430
|
+
for (let index = jobsIndex + 1; index < jobsEnd; index++) {
|
|
431
|
+
const entry = getYamlKeyEntry(lines[index], index)
|
|
432
|
+
if (entry) entries.push(entry)
|
|
433
|
+
}
|
|
434
|
+
const jobIndent = Math.min(...entries.map(entry => entry.indent))
|
|
435
|
+
const selected = entries.find(entry => entry.indent === jobIndent && entry.key === jobName)
|
|
436
|
+
if (selected) return getYamlBlock(lines, selected.index, selected.indent)
|
|
437
|
+
return
|
|
637
438
|
}
|
|
638
439
|
|
|
639
|
-
|
|
640
|
-
|
|
440
|
+
if (!/^\.gitlab-ci\.ya?ml$/i.test(path.basename(ci.configFile))) return
|
|
441
|
+
const selected = lines
|
|
442
|
+
.map((line, index) => getYamlKeyEntry(line, index))
|
|
443
|
+
.find(entry => entry?.indent === 0 && entry.key === jobName)
|
|
444
|
+
if (selected) return getYamlBlock(lines, selected.index, selected.indent)
|
|
641
445
|
}
|
|
642
446
|
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
...(framework.ciWiring?.commandChain || []),
|
|
652
|
-
].filter(Boolean).join('\n')
|
|
653
|
-
|
|
654
|
-
if (/\bnx\b/i.test(commandText) || hasProbeTool(probe, 'nx')) {
|
|
655
|
-
findings.push({
|
|
656
|
-
id: 'nx-executor-env-forwarding',
|
|
657
|
-
tool: 'nx',
|
|
658
|
-
reason: 'Nx executors and wrapper scripts can sit between the CI command and the final test runner.',
|
|
659
|
-
recommendation: 'Verify that NODE_OPTIONS and Datadog environment variables are preserved by every Nx ' +
|
|
660
|
-
'target, executor, and wrapper that spawns the test runner.',
|
|
661
|
-
})
|
|
447
|
+
function getYamlKeyEntry (line, index) {
|
|
448
|
+
if (!line || /^\s*(?:#|$)/.test(line) || /^\s/.test(line) && line.includes('\t')) return
|
|
449
|
+
const match = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^:#][^:]*)):\s*(?:#.*)?$/.exec(line)
|
|
450
|
+
if (!match) return
|
|
451
|
+
return {
|
|
452
|
+
indent: match[1].length,
|
|
453
|
+
index,
|
|
454
|
+
key: String(match[2] ?? match[3] ?? match[4]).trim(),
|
|
662
455
|
}
|
|
456
|
+
}
|
|
663
457
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
reason: 'Turborepo can filter environment variables for tasks.',
|
|
669
|
-
recommendation: 'Verify turbo.json pass-through settings preserve NODE_OPTIONS and required DD_* variables ' +
|
|
670
|
-
'for test tasks.',
|
|
671
|
-
})
|
|
458
|
+
function normalizeYamlKey (value) {
|
|
459
|
+
const key = String(value || '').trim().replace(/:\s*$/, '').trim()
|
|
460
|
+
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
|
461
|
+
return key.slice(1, -1)
|
|
672
462
|
}
|
|
463
|
+
return key
|
|
464
|
+
}
|
|
673
465
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
tool: 'lage',
|
|
678
|
-
reason: 'Lage can run package scripts through an intermediate task process.',
|
|
679
|
-
recommendation: 'Verify the Lage task and any package script it invokes preserve NODE_OPTIONS and required ' +
|
|
680
|
-
'DD_* variables for the final test runner.',
|
|
681
|
-
})
|
|
682
|
-
}
|
|
466
|
+
function getYamlBlock (lines, start, indent) {
|
|
467
|
+
return lines.slice(start, findYamlBlockEnd(lines, start, indent)).join('\n')
|
|
468
|
+
}
|
|
683
469
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
recommendation: 'Trace the command chain from the CI step to the test runner and find where NODE_OPTIONS is ' +
|
|
690
|
-
'removed or replaced.',
|
|
691
|
-
})
|
|
470
|
+
function findYamlBlockEnd (lines, start, indent) {
|
|
471
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
472
|
+
if (/^\s*(?:#|$)/.test(lines[index])) continue
|
|
473
|
+
const nextIndent = /^\s*/.exec(lines[index])[0].length
|
|
474
|
+
if (nextIndent <= indent) return index
|
|
692
475
|
}
|
|
693
|
-
|
|
694
|
-
return findings
|
|
476
|
+
return lines.length
|
|
695
477
|
}
|
|
696
478
|
|
|
697
479
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
480
|
+
* Returns missing fields required for a conclusive static audit.
|
|
481
|
+
*
|
|
482
|
+
* @param {object} ci CI evidence
|
|
483
|
+
* @returns {string[]} missing field labels
|
|
700
484
|
*/
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
]
|
|
707
|
-
return signals.some(signal => signal.name === name)
|
|
485
|
+
function getMissingReviewFields (ci) {
|
|
486
|
+
return [
|
|
487
|
+
['CI file', ci.configFile],
|
|
488
|
+
['job', ci.job],
|
|
489
|
+
['exact command', ci.command],
|
|
490
|
+
].filter(([, value]) => typeof value !== 'string' || !value.trim()).map(([label]) => label)
|
|
708
491
|
}
|
|
709
492
|
|
|
710
493
|
/**
|
|
711
|
-
*
|
|
494
|
+
* Reads one bounded regular CI file.
|
|
495
|
+
*
|
|
496
|
+
* @param {string} filename CI file
|
|
497
|
+
* @returns {string|undefined} source
|
|
712
498
|
*/
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
seen.add(signal.name)
|
|
720
|
-
names.push(signal.name)
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (names.length === 0) return ''
|
|
724
|
-
if (names.length === 1) return names[0]
|
|
725
|
-
return `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`
|
|
499
|
+
function readCiSource (filename) {
|
|
500
|
+
try {
|
|
501
|
+
const stat = fs.lstatSync(filename)
|
|
502
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_CI_FILE_BYTES) return
|
|
503
|
+
return fs.readFileSync(filename, 'utf8')
|
|
504
|
+
} catch {}
|
|
726
505
|
}
|
|
727
506
|
|
|
728
507
|
/**
|
|
729
|
-
*
|
|
508
|
+
* Checks whether recorded text appears literally after line-ending normalization.
|
|
509
|
+
*
|
|
510
|
+
* @param {string} source file source
|
|
511
|
+
* @param {string} value recorded value
|
|
512
|
+
* @returns {boolean} literal presence
|
|
730
513
|
*/
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
jest: 'Jest',
|
|
736
|
-
mocha: 'Mocha',
|
|
737
|
-
playwright: 'Playwright',
|
|
738
|
-
vitest: 'Vitest',
|
|
739
|
-
}[frameworkName] || frameworkName || 'test runner'
|
|
514
|
+
function containsLiteral (source, value) {
|
|
515
|
+
const normalizedSource = source.replaceAll('\r\n', '\n')
|
|
516
|
+
const normalizedValue = String(value).replaceAll('\r\n', '\n').trim()
|
|
517
|
+
return normalizedValue !== '' && normalizedSource.includes(normalizedValue)
|
|
740
518
|
}
|
|
741
519
|
|
|
742
|
-
function
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
/\bTasks:\s*[1-9]\d*\s+successful\b/i.test(line)
|
|
520
|
+
function containsExecutionCommand (source, value) {
|
|
521
|
+
const command = String(value).replaceAll('\r\n', '\n').trim()
|
|
522
|
+
if (!command || command.includes('\n')) return false
|
|
523
|
+
return source.split(/\r?\n/).some(line => {
|
|
524
|
+
if (/^\s*#/.test(line)) return false
|
|
525
|
+
const match = /^\s*(?:-\s*)?(?:run|script):\s*(.*?)\s*$/.exec(line)
|
|
526
|
+
return match?.[1] === command
|
|
750
527
|
})
|
|
751
528
|
}
|
|
752
529
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Builds an incomplete CI result.
|
|
532
|
+
*
|
|
533
|
+
* @param {object} framework framework entry
|
|
534
|
+
* @param {string} diagnosis diagnosis
|
|
535
|
+
* @param {object} evidence evidence
|
|
536
|
+
* @returns {object} result
|
|
537
|
+
*/
|
|
538
|
+
function getIncomplete (framework, diagnosis, evidence) {
|
|
539
|
+
return incomplete(framework, 'ci-wiring', diagnosis, {
|
|
540
|
+
ciWiring: framework.ciWiring,
|
|
541
|
+
...evidence,
|
|
542
|
+
})
|
|
757
543
|
}
|
|
758
544
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
stdoutSummary: preflight.stdoutSummary,
|
|
773
|
-
stderrSummary: preflight.stderrSummary,
|
|
774
|
-
}
|
|
545
|
+
/**
|
|
546
|
+
* Builds a confirmed CI failure.
|
|
547
|
+
*
|
|
548
|
+
* @param {object} framework framework entry
|
|
549
|
+
* @param {string} diagnosis diagnosis
|
|
550
|
+
* @param {object} evidence evidence
|
|
551
|
+
* @returns {object} result
|
|
552
|
+
*/
|
|
553
|
+
function getFailure (framework, diagnosis, evidence) {
|
|
554
|
+
return fail(framework, 'ci-wiring', diagnosis, {
|
|
555
|
+
ciWiring: framework.ciWiring,
|
|
556
|
+
...evidence,
|
|
557
|
+
})
|
|
775
558
|
}
|
|
776
559
|
|
|
777
|
-
module.exports = {
|
|
778
|
-
getCiWiringCommand,
|
|
779
|
-
runCiWiring,
|
|
780
|
-
}
|
|
560
|
+
module.exports = { runCiWiring }
|