dd-trace 6.4.0 → 6.6.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/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +11 -8
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +31 -19
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +44 -17
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +162 -46
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +3 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-playwright/src/index.js +181 -65
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
- package/packages/dd-trace/src/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
const { getCommandOutputPaths } = require('./command-output-policy')
|
|
8
|
+
const { getCommandExecutionSettings } = require('./command-runner')
|
|
9
|
+
const { bindManifestExecutables, getManifestCommands } = require('./executable')
|
|
10
|
+
const { getFixtureRecipeDigests } = require('./offline-fixtures')
|
|
11
|
+
const { sanitizeForReport } = require('./redaction')
|
|
12
|
+
|
|
13
|
+
const APPROVAL_DIGEST_PATTERN = /^[a-f0-9]{64}$/
|
|
14
|
+
const OFFLINE_FIXTURE_NONCE_PATTERN = /^[a-f0-9]{32}$/
|
|
15
|
+
const PACKAGE_SNAPSHOT_EXCLUDED_NAMES = new Set(['.git', '.nyc_output', 'node_modules'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Binds an approval to the exact manifest bytes and live validator options.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} input approval inputs
|
|
21
|
+
* @param {object} input.manifest loaded manifest
|
|
22
|
+
* @param {string} input.out validation output directory
|
|
23
|
+
* @param {string[]} [input.selectedFrameworkIds] selected framework ids
|
|
24
|
+
* @param {string|null} [input.requestedScenario] selected scenario
|
|
25
|
+
* @param {string} input.offlineFixtureNonce random fixture-root nonce shown in the execution plan
|
|
26
|
+
* @param {boolean} [input.keepTempFiles] whether generated files are retained
|
|
27
|
+
* @param {boolean} [input.verbose] whether command progress is printed
|
|
28
|
+
* @returns {string} SHA-256 approval digest
|
|
29
|
+
*/
|
|
30
|
+
function getApprovalDigest ({
|
|
31
|
+
manifest,
|
|
32
|
+
out,
|
|
33
|
+
selectedFrameworkIds = [],
|
|
34
|
+
requestedScenario = null,
|
|
35
|
+
offlineFixtureNonce,
|
|
36
|
+
keepTempFiles = false,
|
|
37
|
+
verbose = false,
|
|
38
|
+
}) {
|
|
39
|
+
const approvalJson = serializeApprovalMaterial({
|
|
40
|
+
manifest,
|
|
41
|
+
out,
|
|
42
|
+
selectedFrameworkIds,
|
|
43
|
+
requestedScenario,
|
|
44
|
+
offlineFixtureNonce,
|
|
45
|
+
keepTempFiles,
|
|
46
|
+
verbose,
|
|
47
|
+
})
|
|
48
|
+
return crypto.createHash('sha256').update(approvalJson).digest('hex')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds the complete, inspectable material covered by one approval fingerprint.
|
|
53
|
+
*
|
|
54
|
+
* Secret-like values are redacted for the artifact while the raw manifest digest still binds their exact bytes.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} input approval inputs
|
|
57
|
+
* @param {object} input.manifest loaded validation manifest
|
|
58
|
+
* @param {string} input.out validation output directory
|
|
59
|
+
* @param {string[]} [input.selectedFrameworkIds] selected framework identifiers
|
|
60
|
+
* @param {string|null} [input.requestedScenario] selected validation scenario
|
|
61
|
+
* @param {string} input.offlineFixtureNonce private offline fixture nonce
|
|
62
|
+
* @param {boolean} [input.keepTempFiles] whether generated files remain after validation
|
|
63
|
+
* @param {boolean} [input.verbose] whether verbose validation output is enabled
|
|
64
|
+
* @returns {object} deterministic approval material
|
|
65
|
+
*/
|
|
66
|
+
function getApprovalMaterial ({
|
|
67
|
+
manifest,
|
|
68
|
+
out,
|
|
69
|
+
selectedFrameworkIds = [],
|
|
70
|
+
requestedScenario = null,
|
|
71
|
+
offlineFixtureNonce,
|
|
72
|
+
keepTempFiles = false,
|
|
73
|
+
verbose = false,
|
|
74
|
+
}) {
|
|
75
|
+
if (!OFFLINE_FIXTURE_NONCE_PATTERN.test(String(offlineFixtureNonce || ''))) {
|
|
76
|
+
throw new Error('Invalid offline fixture nonce. Render a fresh plan with --print-plan.')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const validationDirectory = __dirname
|
|
80
|
+
const packageRoot = path.resolve(validationDirectory, '..', '..')
|
|
81
|
+
const packageJsonPath = path.join(packageRoot, 'package.json')
|
|
82
|
+
const packageMetadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
83
|
+
const packageFiles = getPackageFiles(packageRoot, [
|
|
84
|
+
manifest.__path,
|
|
85
|
+
out,
|
|
86
|
+
path.join(packageRoot, '.junit-tmp'),
|
|
87
|
+
])
|
|
88
|
+
const executableIdentities = bindManifestExecutables(manifest)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
sharingWarning: 'Internal diagnostic material. Review repository paths, commands, and CI metadata before sharing.',
|
|
93
|
+
validator: {
|
|
94
|
+
package: packageMetadata.name,
|
|
95
|
+
version: packageMetadata.version,
|
|
96
|
+
packageRoot,
|
|
97
|
+
coveredFiles: packageFiles.map(filename => ({
|
|
98
|
+
path: path.relative(packageRoot, filename).split(path.sep).join('/'),
|
|
99
|
+
sha256: getFileDigest(filename),
|
|
100
|
+
})),
|
|
101
|
+
},
|
|
102
|
+
manifest: {
|
|
103
|
+
path: path.resolve(manifest.__path),
|
|
104
|
+
sha256: getManifestDigest(manifest),
|
|
105
|
+
},
|
|
106
|
+
selection: {
|
|
107
|
+
frameworks: [...selectedFrameworkIds],
|
|
108
|
+
scenario: requestedScenario,
|
|
109
|
+
},
|
|
110
|
+
validation: {
|
|
111
|
+
outputDirectory: path.resolve(out),
|
|
112
|
+
offlineFixtureNonce,
|
|
113
|
+
keepTemporaryFiles: keepTempFiles,
|
|
114
|
+
verbose,
|
|
115
|
+
},
|
|
116
|
+
fixtureRecipeDigests: getFixtureRecipeDigests({
|
|
117
|
+
frameworks: manifest.frameworks || [],
|
|
118
|
+
selectedFrameworkIds,
|
|
119
|
+
requestedScenario,
|
|
120
|
+
}),
|
|
121
|
+
commands: getManifestCommands(manifest).map(([id, command]) => getApprovalCommand(id, command)),
|
|
122
|
+
generatedFiles: getGeneratedFileMaterial(manifest),
|
|
123
|
+
executables: executableIdentities,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Serializes approval material using stable formatting suitable for independent SHA-256 tools.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} input approval inputs
|
|
131
|
+
* @returns {string} UTF-8 JSON text ending in one newline
|
|
132
|
+
*/
|
|
133
|
+
function serializeApprovalMaterial (input) {
|
|
134
|
+
return `${JSON.stringify(getApprovalMaterial(input), null, 2)}\n`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns every regular file owned by the installed dd-trace package.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} packageRoot installed dd-trace package root
|
|
141
|
+
* @param {string[]} excludedPaths generated files or directories outside the package snapshot
|
|
142
|
+
* @returns {string[]} sorted absolute file paths
|
|
143
|
+
*/
|
|
144
|
+
function getPackageFiles (packageRoot, excludedPaths) {
|
|
145
|
+
const files = []
|
|
146
|
+
collectPackageFiles(
|
|
147
|
+
fs.realpathSync(packageRoot),
|
|
148
|
+
excludedPaths.map(resolvePhysicalPath),
|
|
149
|
+
files
|
|
150
|
+
)
|
|
151
|
+
return files.sort()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Converts one manifest command into its sanitized, execution-relevant approval shape.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} id stable command identifier
|
|
158
|
+
* @param {object} command structured command
|
|
159
|
+
* @returns {object} command approval material
|
|
160
|
+
*/
|
|
161
|
+
function getApprovalCommand (id, command) {
|
|
162
|
+
const shape = {
|
|
163
|
+
id,
|
|
164
|
+
required: command.required !== false,
|
|
165
|
+
usesShell: command.usesShell === true,
|
|
166
|
+
cwd: path.resolve(command.cwd),
|
|
167
|
+
environmentMode: 'clean',
|
|
168
|
+
environment: command.env || {},
|
|
169
|
+
...getCommandExecutionSettings(command),
|
|
170
|
+
outputPaths: getCommandOutputPaths(command),
|
|
171
|
+
}
|
|
172
|
+
if (command.usesShell) {
|
|
173
|
+
shape.shell = command.shell || null
|
|
174
|
+
shape.shellCommand = command.shellCommand
|
|
175
|
+
} else {
|
|
176
|
+
shape.argv = command.argv
|
|
177
|
+
}
|
|
178
|
+
return sanitizeForReport(shape)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns exact generated test source and cleanup policy covered by the manifest digest.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} manifest loaded manifest
|
|
185
|
+
* @returns {object[]} generated file approval material
|
|
186
|
+
*/
|
|
187
|
+
function getGeneratedFileMaterial (manifest) {
|
|
188
|
+
const files = []
|
|
189
|
+
for (const framework of manifest.frameworks || []) {
|
|
190
|
+
const strategy = framework.generatedTestStrategy
|
|
191
|
+
for (const file of strategy?.files || []) {
|
|
192
|
+
const content = `${file.contentLines.join('\n')}\n`
|
|
193
|
+
files.push(sanitizeForReport({
|
|
194
|
+
frameworkId: framework.id,
|
|
195
|
+
path: path.resolve(file.path),
|
|
196
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
197
|
+
content,
|
|
198
|
+
removeAfterValidation: (strategy.cleanupPaths || []).some(cleanupPath => {
|
|
199
|
+
return path.resolve(cleanupPath) === path.resolve(file.path)
|
|
200
|
+
}),
|
|
201
|
+
}))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return files
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Hashes one covered regular file.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} filename absolute filename
|
|
211
|
+
* @returns {string} lowercase SHA-256 digest
|
|
212
|
+
*/
|
|
213
|
+
function getFileDigest (filename) {
|
|
214
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filename)).digest('hex')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collects regular package files without following package-internal symbolic links.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} directory current package directory
|
|
221
|
+
* @param {string[]} excludedPaths generated paths omitted from the package snapshot
|
|
222
|
+
* @param {string[]} files collected files
|
|
223
|
+
*/
|
|
224
|
+
function collectPackageFiles (directory, excludedPaths, files) {
|
|
225
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
226
|
+
const filename = path.join(directory, entry.name)
|
|
227
|
+
if (PACKAGE_SNAPSHOT_EXCLUDED_NAMES.has(entry.name) || isExcludedPackagePath(filename, excludedPaths)) continue
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
collectPackageFiles(filename, excludedPaths, files)
|
|
230
|
+
} else if (entry.isFile()) {
|
|
231
|
+
files.push(filename)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Checks whether a package path belongs to a generated approval input or output.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} filename package path
|
|
240
|
+
* @param {string[]} excludedPaths generated paths omitted from the package snapshot
|
|
241
|
+
* @returns {boolean} whether the path is excluded
|
|
242
|
+
*/
|
|
243
|
+
function isExcludedPackagePath (filename, excludedPaths) {
|
|
244
|
+
return excludedPaths.some(excluded => filename === excluded || filename.startsWith(`${excluded}${path.sep}`))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolves an existing path or its nearest existing ancestor through filesystem aliases.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} filename path that may not exist yet
|
|
251
|
+
* @returns {string} physical path
|
|
252
|
+
*/
|
|
253
|
+
function resolvePhysicalPath (filename) {
|
|
254
|
+
const missingSegments = []
|
|
255
|
+
let existingPath = path.resolve(filename)
|
|
256
|
+
while (!fs.existsSync(existingPath)) {
|
|
257
|
+
missingSegments.unshift(path.basename(existingPath))
|
|
258
|
+
const parent = path.dirname(existingPath)
|
|
259
|
+
if (parent === existingPath) return path.resolve(filename)
|
|
260
|
+
existingPath = parent
|
|
261
|
+
}
|
|
262
|
+
return path.join(fs.realpathSync(existingPath), ...missingSegments)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validates an approval digest before live validation executes project code.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} digest supplied approval digest
|
|
269
|
+
* @param {object} input approval inputs
|
|
270
|
+
* @returns {void}
|
|
271
|
+
*/
|
|
272
|
+
function assertApprovalDigest (digest, input) {
|
|
273
|
+
if (!APPROVAL_DIGEST_PATTERN.test(String(digest || ''))) {
|
|
274
|
+
throw new Error('Invalid approved plan SHA-256. Render a fresh plan with --print-plan.')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const expected = getApprovalDigest(input)
|
|
278
|
+
if (digest !== expected) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'The validation manifest or execution options changed after approval. ' +
|
|
281
|
+
'Render a fresh plan with --print-plan and approve that exact plan before live validation.'
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getManifestDigest (manifest) {
|
|
287
|
+
if (manifest.__sourceSha256) return manifest.__sourceSha256
|
|
288
|
+
|
|
289
|
+
const serializable = { ...manifest }
|
|
290
|
+
delete serializable.__path
|
|
291
|
+
return crypto.createHash('sha256').update(JSON.stringify(serializable)).digest('hex')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
assertApprovalDigest,
|
|
296
|
+
getApprovalDigest,
|
|
297
|
+
getApprovalMaterial,
|
|
298
|
+
serializeApprovalMaterial,
|
|
299
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
|
|
5
|
+
const MAX_PREFIX_LENGTH = 72
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Maps an arbitrary manifest identifier to a bounded, portable, collision-resistant path segment.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} value original identifier
|
|
11
|
+
* @returns {string} artifact path segment
|
|
12
|
+
*/
|
|
13
|
+
function getArtifactId (value) {
|
|
14
|
+
const source = String(value)
|
|
15
|
+
const prefix = source.replaceAll(/[^a-zA-Z0-9._-]+/g, '-').slice(0, MAX_PREFIX_LENGTH) || 'framework'
|
|
16
|
+
const digest = crypto.createHash('sha256').update(source).digest('hex').slice(0, 12)
|
|
17
|
+
return `${prefix}-${digest}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { getArtifactId }
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_COLLECTION_ENTRIES = 100_000
|
|
4
|
+
const DEFAULT_MAX_NESTING_DEPTH = 128
|
|
5
|
+
const DEFAULT_MAX_STRING_BYTES = 64 * 1024
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses JSON only after a no-allocation scan bounds nesting, strings, and collection cardinality.
|
|
9
|
+
*
|
|
10
|
+
* @param {Buffer|string} source encoded JSON
|
|
11
|
+
* @param {object} [options] parser limits
|
|
12
|
+
* @param {string} [options.label] value label used in errors
|
|
13
|
+
* @param {number} [options.maxCollectionEntries] aggregate array/object entries
|
|
14
|
+
* @param {number} [options.maxNestingDepth] maximum container nesting
|
|
15
|
+
* @param {number} [options.maxStringBytes] maximum encoded string bytes
|
|
16
|
+
* @returns {{collectionEntries: number, value: unknown}} parsed value and scanned cardinality
|
|
17
|
+
*/
|
|
18
|
+
function parseBoundedJson (source, options = {}) {
|
|
19
|
+
const buffer = Buffer.isBuffer(source) ? source : Buffer.from(String(source))
|
|
20
|
+
const limits = {
|
|
21
|
+
label: options.label || 'JSON',
|
|
22
|
+
maxCollectionEntries: options.maxCollectionEntries || DEFAULT_MAX_COLLECTION_ENTRIES,
|
|
23
|
+
maxNestingDepth: options.maxNestingDepth || DEFAULT_MAX_NESTING_DEPTH,
|
|
24
|
+
maxStringBytes: options.maxStringBytes || DEFAULT_MAX_STRING_BYTES,
|
|
25
|
+
}
|
|
26
|
+
const collectionEntries = scanJson(buffer, limits)
|
|
27
|
+
return {
|
|
28
|
+
collectionEntries,
|
|
29
|
+
value: JSON.parse(buffer.toString('utf8')),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scans JSON bytes without creating one object per delimiter.
|
|
35
|
+
*
|
|
36
|
+
* @param {Buffer} buffer JSON bytes
|
|
37
|
+
* @param {object} limits parser limits
|
|
38
|
+
* @returns {number} aggregate collection entries
|
|
39
|
+
*/
|
|
40
|
+
function scanJson (buffer, limits) {
|
|
41
|
+
const containerTypes = new Uint8Array(limits.maxNestingDepth)
|
|
42
|
+
const arrayHasEntry = new Uint8Array(limits.maxNestingDepth)
|
|
43
|
+
let collectionEntries = 0
|
|
44
|
+
let depth = 0
|
|
45
|
+
let escaped = false
|
|
46
|
+
let inString = false
|
|
47
|
+
let stringBytes = 0
|
|
48
|
+
|
|
49
|
+
for (const byte of buffer) {
|
|
50
|
+
if (inString) {
|
|
51
|
+
stringBytes++
|
|
52
|
+
if (stringBytes > limits.maxStringBytes) {
|
|
53
|
+
throw new Error(`${limits.label} contains a string larger than ${limits.maxStringBytes} bytes.`)
|
|
54
|
+
}
|
|
55
|
+
if (escaped) {
|
|
56
|
+
escaped = false
|
|
57
|
+
} else if (byte === 0x5C) {
|
|
58
|
+
escaped = true
|
|
59
|
+
} else if (byte === 0x22) {
|
|
60
|
+
inString = false
|
|
61
|
+
}
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (byte === 0x22) {
|
|
66
|
+
inString = true
|
|
67
|
+
stringBytes = 0
|
|
68
|
+
markArrayEntry()
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const containerIndex = depth - 1
|
|
73
|
+
if (containerIndex >= 0 && containerTypes[containerIndex] === 1 &&
|
|
74
|
+
arrayHasEntry[containerIndex] === 0 && !isJsonWhitespace(byte) && byte !== 0x5D) {
|
|
75
|
+
arrayHasEntry[containerIndex] = 1
|
|
76
|
+
addEntry()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (byte === 0x5B || byte === 0x7B) {
|
|
80
|
+
if (depth >= limits.maxNestingDepth) {
|
|
81
|
+
throw new Error(`${limits.label} nesting exceeds ${limits.maxNestingDepth}.`)
|
|
82
|
+
}
|
|
83
|
+
containerTypes[depth] = byte === 0x5B ? 1 : 2
|
|
84
|
+
arrayHasEntry[depth] = 0
|
|
85
|
+
depth++
|
|
86
|
+
} else if (byte === 0x5D || byte === 0x7D) {
|
|
87
|
+
if (depth > 0) depth--
|
|
88
|
+
} else if (byte === 0x2C && containerIndex >= 0 && containerTypes[containerIndex] === 1) {
|
|
89
|
+
arrayHasEntry[containerIndex] = 0
|
|
90
|
+
} else if (byte === 0x3A && containerIndex >= 0 && containerTypes[containerIndex] === 2) {
|
|
91
|
+
addEntry()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return collectionEntries
|
|
96
|
+
|
|
97
|
+
function markArrayEntry () {
|
|
98
|
+
const index = depth - 1
|
|
99
|
+
if (index < 0 || containerTypes[index] !== 1 || arrayHasEntry[index] !== 0) return
|
|
100
|
+
arrayHasEntry[index] = 1
|
|
101
|
+
addEntry()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addEntry () {
|
|
105
|
+
collectionEntries++
|
|
106
|
+
if (collectionEntries > limits.maxCollectionEntries) {
|
|
107
|
+
throw new Error(`${limits.label} exceeds ${limits.maxCollectionEntries} aggregate collection entries.`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isJsonWhitespace (byte) {
|
|
113
|
+
return byte === 0x20 || byte === 0x09 || byte === 0x0A || byte === 0x0D
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
DEFAULT_MAX_COLLECTION_ENTRIES,
|
|
118
|
+
DEFAULT_MAX_NESTING_DEPTH,
|
|
119
|
+
DEFAULT_MAX_STRING_BYTES,
|
|
120
|
+
parseBoundedJson,
|
|
121
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getCommandDetails,
|
|
5
|
+
serializeDisplayCommand,
|
|
6
|
+
} = require('./command-runner')
|
|
7
|
+
const { sanitizeEnv } = require('./redaction')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the normalized CI command metadata shape shared by reports and UI payloads.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} framework manifest framework entry
|
|
13
|
+
* @returns {object|undefined} CI command candidate context when available
|
|
14
|
+
*/
|
|
15
|
+
function buildCiCommandCandidate (framework) {
|
|
16
|
+
const ciWiring = framework.ciWiring || {}
|
|
17
|
+
const command = framework.ciWiringCommand
|
|
18
|
+
|
|
19
|
+
if (!command && !hasCiWiringContext(ciWiring)) return
|
|
20
|
+
|
|
21
|
+
return removeUndefined({
|
|
22
|
+
provider: ciWiring.provider || undefined,
|
|
23
|
+
configFile: ciWiring.configFile || undefined,
|
|
24
|
+
workflow: ciWiring.workflow || undefined,
|
|
25
|
+
job: ciWiring.job || undefined,
|
|
26
|
+
step: ciWiring.step || undefined,
|
|
27
|
+
runner: ciWiring.runner || undefined,
|
|
28
|
+
shell: ciWiring.shell || undefined,
|
|
29
|
+
command: command ? serializeDisplayCommand(command) : ciWiring.command,
|
|
30
|
+
cwd: command?.cwd || ciWiring.workingDirectory,
|
|
31
|
+
whySelected: ciWiring.whySelected || ciWiring.selectionReason || ciWiring.diagnosis,
|
|
32
|
+
replayability: ciWiring.replayability,
|
|
33
|
+
replayBlocker: ciWiring.replayBlocker,
|
|
34
|
+
initialization: ciWiring.initialization,
|
|
35
|
+
env: buildCiEnvSummary(ciWiring, command),
|
|
36
|
+
packageScriptExpansionChain: getFirstArray(
|
|
37
|
+
ciWiring.packageScriptExpansionChain,
|
|
38
|
+
ciWiring.scriptExpansionChain,
|
|
39
|
+
ciWiring.commandExpansion
|
|
40
|
+
),
|
|
41
|
+
runnerToolChain: getFirstArray(
|
|
42
|
+
ciWiring.runnerToolChain,
|
|
43
|
+
ciWiring.toolChain,
|
|
44
|
+
ciWiring.commandChain
|
|
45
|
+
),
|
|
46
|
+
setupCommandIds: ciWiring.setupCommandIds,
|
|
47
|
+
unresolved: ciWiring.unresolved,
|
|
48
|
+
commandDetails: command && getCommandDetails(command),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildCiEnvSummary (ciWiring, command) {
|
|
53
|
+
const summary = removeUndefined({
|
|
54
|
+
workflow: sanitizeEnv(ciWiring.workflowEnv || ciWiring.env?.workflow),
|
|
55
|
+
job: sanitizeEnv(ciWiring.jobEnv || ciWiring.env?.job),
|
|
56
|
+
step: sanitizeEnv(ciWiring.stepEnv || command?.env || ciWiring.env?.step),
|
|
57
|
+
inherited: sanitizeEnv(ciWiring.inheritedEnv),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return Object.keys(summary).length > 0 ? summary : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasCiWiringContext (ciWiring) {
|
|
64
|
+
return Object.keys(ciWiring).length > 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getFirstArray (...values) {
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
if (Array.isArray(value) && value.length > 0) return value
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function removeUndefined (object) {
|
|
74
|
+
const result = {}
|
|
75
|
+
for (const [key, value] of Object.entries(object)) {
|
|
76
|
+
if (value !== undefined) result[key] = value
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
buildCiCommandCandidate,
|
|
83
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CI_SEARCHES = [
|
|
6
|
+
'.github/workflows/*.yml',
|
|
7
|
+
'.github/workflows/*.yaml',
|
|
8
|
+
'.gitlab-ci.yml',
|
|
9
|
+
'.gitlab-ci.yaml',
|
|
10
|
+
'.circleci/config.yml',
|
|
11
|
+
'.circleci/config.yaml',
|
|
12
|
+
'.buildkite/pipeline.yml',
|
|
13
|
+
'.buildkite/pipeline.yaml',
|
|
14
|
+
'bitbucket-pipelines.yml',
|
|
15
|
+
'bitbucket-pipelines.yaml',
|
|
16
|
+
'azure-pipelines.yml',
|
|
17
|
+
'azure-pipelines.yaml',
|
|
18
|
+
'.azure-pipelines/*.yml',
|
|
19
|
+
'.azure-pipelines/*.yaml',
|
|
20
|
+
'Jenkinsfile',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function annotateCiDiscovery ({ manifest, diagnosis }) {
|
|
24
|
+
manifest.ciDiscovery = buildCiDiscovery({ manifest, diagnosis })
|
|
25
|
+
|
|
26
|
+
if (manifest.ciDiscovery.contradictions.length === 0) return
|
|
27
|
+
|
|
28
|
+
const warnings = Array.isArray(manifest.warnings) ? manifest.warnings : []
|
|
29
|
+
for (const contradiction of manifest.ciDiscovery.contradictions) {
|
|
30
|
+
const warning = `CI discovery contradiction: ${contradiction}`
|
|
31
|
+
if (!warnings.includes(warning)) warnings.push(warning)
|
|
32
|
+
}
|
|
33
|
+
manifest.warnings = warnings
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildCiDiscovery ({ manifest, diagnosis }) {
|
|
37
|
+
const declared = isObject(manifest.ciDiscovery) ? manifest.ciDiscovery : {}
|
|
38
|
+
const staticFound = getStaticWorkflowLocations(diagnosis)
|
|
39
|
+
const declaredFound = normalizeStringArray(declared.found)
|
|
40
|
+
const candidateFound = getManifestWorkflowLocations(manifest)
|
|
41
|
+
const manifestFound = uniqueStrings([...declaredFound, ...candidateFound])
|
|
42
|
+
const searched = normalizeStringArray(declared.searched)
|
|
43
|
+
let method = 'validator-static-diagnosis'
|
|
44
|
+
if (candidateFound.length > 0) method = 'framework-ci-command'
|
|
45
|
+
if (declaredFound.length > 0) method = 'manifest'
|
|
46
|
+
if (typeof declared.method === 'string' && declared.method) method = declared.method
|
|
47
|
+
const found = manifestFound.length > 0 ? manifestFound : staticFound
|
|
48
|
+
const contradictions = getCiDiscoveryContradictions({ manifest, declaredFound: manifestFound, staticFound })
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
searched: searched.length > 0 ? searched : DEFAULT_CI_SEARCHES,
|
|
52
|
+
found,
|
|
53
|
+
staticFound,
|
|
54
|
+
method,
|
|
55
|
+
warnings: normalizeStringArray(declared.warnings),
|
|
56
|
+
notes: normalizeStringArray(declared.notes),
|
|
57
|
+
contradictions,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getManifestWorkflowLocations (manifest) {
|
|
62
|
+
const root = manifest.repository?.root
|
|
63
|
+
const locations = []
|
|
64
|
+
for (const framework of manifest.frameworks || []) {
|
|
65
|
+
const configFile = framework.ciWiring?.configFile
|
|
66
|
+
if (typeof configFile !== 'string') continue
|
|
67
|
+
if (!root || !path.isAbsolute(configFile)) {
|
|
68
|
+
locations.push(configFile)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const relative = path.relative(root, configFile)
|
|
73
|
+
locations.push(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
74
|
+
? relative.split(path.sep).join('/')
|
|
75
|
+
: configFile)
|
|
76
|
+
}
|
|
77
|
+
return uniqueStrings(locations)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getFrameworkCiDiscoveryContradiction (framework, manifest) {
|
|
81
|
+
const ciDiscovery = manifest?.ciDiscovery
|
|
82
|
+
if (!ciDiscovery || !Array.isArray(ciDiscovery.staticFound) || ciDiscovery.staticFound.length === 0) return null
|
|
83
|
+
if (!frameworkClaimsNoCi(framework)) return null
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
reason: 'CI workflow files were found by validator static diagnosis, but this manifest entry says no CI ' +
|
|
87
|
+
'workflow was found. The manifest cannot support a "no CI workflow found" conclusion.',
|
|
88
|
+
recommendation: 'Inspect the discovered CI files with hidden-directory-aware discovery, then update ciWiring, ' +
|
|
89
|
+
'ciWiringCommand, omittedTestCommands, notes, or unresolved blockers before rerunning live validation.',
|
|
90
|
+
ciDiscovery,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function manifestHasCiDiscoveryContradiction (manifest) {
|
|
95
|
+
return Array.isArray(manifest?.ciDiscovery?.contradictions) &&
|
|
96
|
+
manifest.ciDiscovery.contradictions.length > 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getCiDiscoveryContradictions ({ manifest, declaredFound, staticFound }) {
|
|
100
|
+
if (staticFound.length === 0) return []
|
|
101
|
+
|
|
102
|
+
const contradictions = []
|
|
103
|
+
if (isObject(manifest.ciDiscovery) && declaredFound.length === 0) {
|
|
104
|
+
contradictions.push(
|
|
105
|
+
`manifest ciDiscovery.found is empty, but static diagnosis found ${formatList(staticFound)}`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const framework of manifest.frameworks || []) {
|
|
110
|
+
if (!frameworkClaimsNoCi(framework)) continue
|
|
111
|
+
contradictions.push(
|
|
112
|
+
`framework ${framework.id || '<unknown>'} records no CI workflow, but static diagnosis found ` +
|
|
113
|
+
formatList(staticFound)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return contradictions
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function frameworkClaimsNoCi (framework) {
|
|
121
|
+
const ciWiring = framework?.ciWiring
|
|
122
|
+
if (!isObject(ciWiring)) return false
|
|
123
|
+
if (ciWiring.provider === 'none') return true
|
|
124
|
+
|
|
125
|
+
return textClaimsNoCi([
|
|
126
|
+
ciWiring.diagnosis,
|
|
127
|
+
...(Array.isArray(ciWiring.unresolved) ? ciWiring.unresolved : []),
|
|
128
|
+
...(Array.isArray(framework.notes) ? framework.notes : []),
|
|
129
|
+
])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function textClaimsNoCi (values) {
|
|
133
|
+
return values.some(value => {
|
|
134
|
+
if (typeof value !== 'string') return false
|
|
135
|
+
return /no .*ci .*workflow/i.test(value) ||
|
|
136
|
+
/no .*ci .*configuration/i.test(value) ||
|
|
137
|
+
/no github actions.*gitlab.*circleci.*jenkins/i.test(value)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getStaticWorkflowLocations (diagnosis) {
|
|
142
|
+
const results = Array.isArray(diagnosis?.results) ? diagnosis.results : []
|
|
143
|
+
const locations = []
|
|
144
|
+
const seen = new Set()
|
|
145
|
+
|
|
146
|
+
for (const result of results) {
|
|
147
|
+
if (result.title !== 'CI workflow files found' || !Array.isArray(result.locations)) continue
|
|
148
|
+
for (const location of result.locations) {
|
|
149
|
+
if (typeof location !== 'string' || seen.has(location)) continue
|
|
150
|
+
seen.add(location)
|
|
151
|
+
locations.push(location)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return locations
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeStringArray (value) {
|
|
159
|
+
if (!Array.isArray(value)) return []
|
|
160
|
+
return value.filter(item => typeof item === 'string')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function uniqueStrings (values) {
|
|
164
|
+
return [...new Set(values.filter(value => typeof value === 'string' && value !== ''))]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatList (values) {
|
|
168
|
+
return values.map(value => `"${value}"`).join(', ')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isObject (value) {
|
|
172
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
annotateCiDiscovery,
|
|
177
|
+
buildCiDiscovery,
|
|
178
|
+
getFrameworkCiDiscoveryContradiction,
|
|
179
|
+
getStaticWorkflowLocations,
|
|
180
|
+
manifestHasCiDiscoveryContradiction,
|
|
181
|
+
}
|