@decohere/ci-tools 0.0.1-dev.108 → 0.0.1-dev.11
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/package.json +3 -8
- package/src/audit/enforce.mjs +5 -7
- package/src/audit/summarize.mjs +32 -10
- package/src/audit/upsert-issue.mjs +37 -22
- package/src/bench/bench.mjs +3 -13
- package/src/bench/trace.mjs +14 -18
- package/src/coverage/enforce-thresholds.mjs +144 -0
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/release/sync-dependencies.mjs +2 -1
- package/src/audit/format.mjs +0 -133
- package/src/audit/upsert-clickup.mjs +0 -117
- package/src/github/credential-nudge.mjs +0 -320
- package/src/github/oidc-bootstrap-nudge.mjs +0 -368
- package/src/github/publish-verify.mjs +0 -209
- package/src/github/smoke-create-launcher.mjs +0 -70
- package/tests/audit/format.test.mjs +0 -115
- package/tests/github/credential-nudge.test.mjs +0 -65
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -104
- package/tests/github/publish-verify.test.mjs +0 -95
- package/tests/github/smoke-create-launcher.test.mjs +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/ci-tools",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.11",
|
|
4
4
|
"description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -18,25 +18,20 @@
|
|
|
18
18
|
"./lib/lcov": "./src/lib/lcov.mjs",
|
|
19
19
|
"./coverage/badge": "./src/coverage/badge.mjs",
|
|
20
20
|
"./coverage/report": "./src/coverage/report.mjs",
|
|
21
|
+
"./coverage/enforce-thresholds": "./src/coverage/enforce-thresholds.mjs",
|
|
21
22
|
"./badges/ci-status": "./src/badges/ci-status.mjs",
|
|
22
23
|
"./pages/artifact": "./src/pages/artifact.mjs",
|
|
23
24
|
"./pages/smoke": "./src/pages/smoke.mjs",
|
|
24
|
-
"./audit/format": "./src/audit/format.mjs",
|
|
25
25
|
"./audit/summarize": "./src/audit/summarize.mjs",
|
|
26
26
|
"./audit/enforce": "./src/audit/enforce.mjs",
|
|
27
27
|
"./audit/upsert-issue": "./src/audit/upsert-issue.mjs",
|
|
28
|
-
"./audit/upsert-clickup": "./src/audit/upsert-clickup.mjs",
|
|
29
28
|
"./bench/bench": "./src/bench/bench.mjs",
|
|
30
29
|
"./bench/trace": "./src/bench/trace.mjs",
|
|
31
30
|
"./changeset/aggregate-changelog": "./src/changeset/aggregate-changelog.mjs",
|
|
32
31
|
"./changeset/check-status": "./src/changeset/check-status.mjs",
|
|
33
32
|
"./github/create-issue": "./src/github/create-issue.mjs",
|
|
34
33
|
"./github/ensure-metadata": "./src/github/ensure-metadata.mjs",
|
|
35
|
-
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs"
|
|
36
|
-
"./github/publish-verify": "./src/github/publish-verify.mjs",
|
|
37
|
-
"./github/credential-nudge": "./src/github/credential-nudge.mjs",
|
|
38
|
-
"./github/oidc-bootstrap-nudge": "./src/github/oidc-bootstrap-nudge.mjs",
|
|
39
|
-
"./github/smoke-create-launcher": "./src/github/smoke-create-launcher.mjs"
|
|
34
|
+
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs"
|
|
40
35
|
},
|
|
41
36
|
"scripts": {
|
|
42
37
|
"test": "vitest run"
|
package/src/audit/enforce.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { formatLog, highCritical, parseAudit } from "./format.mjs";
|
|
3
2
|
|
|
4
3
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
5
4
|
|
|
@@ -11,13 +10,12 @@ try {
|
|
|
11
10
|
process.exit(1);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const vulnerabilities = data.metadata?.vulnerabilities ?? {};
|
|
14
|
+
const high = vulnerabilities.high ?? 0;
|
|
15
|
+
const critical = vulnerabilities.critical ?? 0;
|
|
16
16
|
|
|
17
|
-
if (
|
|
18
|
-
console.error(
|
|
19
|
-
`Security gate failed: high=${parsed.high}, critical=${parsed.critical}\n${formatLog({ ...parsed, advisories: blocking })}`,
|
|
20
|
-
);
|
|
17
|
+
if (high > 0 || critical > 0) {
|
|
18
|
+
console.error(`Security gate failed: high=${high}, critical=${critical}`);
|
|
21
19
|
process.exit(1);
|
|
22
20
|
}
|
|
23
21
|
|
package/src/audit/summarize.mjs
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
2
|
-
import { formatLog, formatMarkdown, parseAudit } from "./format.mjs";
|
|
3
2
|
|
|
4
3
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
5
4
|
|
|
6
|
-
function writeSummary(
|
|
5
|
+
function writeSummary(lines) {
|
|
6
|
+
const summaryText = `${lines.join("\n")}\n`;
|
|
7
7
|
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
8
|
-
appendFileSync(process.env.GITHUB_STEP_SUMMARY,
|
|
8
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryText);
|
|
9
|
+
return;
|
|
9
10
|
}
|
|
11
|
+
process.stdout.write(summaryText);
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
let data = null;
|
|
13
15
|
try {
|
|
14
16
|
data = JSON.parse(readFileSync(auditFilePath, "utf8"));
|
|
15
17
|
} catch {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
writeSummary([
|
|
19
|
+
"## Security Audit",
|
|
20
|
+
"",
|
|
21
|
+
"- npm audit output could not be parsed.",
|
|
22
|
+
`- See uploaded \`${auditFilePath}\` artifact for raw output.`,
|
|
23
|
+
]);
|
|
24
|
+
process.exit(0);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const vulnerabilities = data.metadata?.vulnerabilities ?? {};
|
|
28
|
+
const info = vulnerabilities.info ?? 0;
|
|
29
|
+
const low = vulnerabilities.low ?? 0;
|
|
30
|
+
const moderate = vulnerabilities.moderate ?? 0;
|
|
31
|
+
const high = vulnerabilities.high ?? 0;
|
|
32
|
+
const critical = vulnerabilities.critical ?? 0;
|
|
33
|
+
const total = vulnerabilities.total ?? info + low + moderate + high + critical;
|
|
34
|
+
|
|
35
|
+
writeSummary([
|
|
36
|
+
"## Security Audit",
|
|
37
|
+
"",
|
|
38
|
+
`| Severity | Count |`,
|
|
39
|
+
`|-----------|-------|`,
|
|
40
|
+
`| Critical | ${critical} |`,
|
|
41
|
+
`| High | ${high} |`,
|
|
42
|
+
`| Moderate | ${moderate} |`,
|
|
43
|
+
`| Low | ${low} |`,
|
|
44
|
+
`| Info | ${info} |`,
|
|
45
|
+
`| **Total** | **${total}** |`,
|
|
46
|
+
]);
|
|
@@ -2,7 +2,6 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { formatMarkdown, parseAudit } from "./format.mjs";
|
|
6
5
|
|
|
7
6
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
8
7
|
const ISSUE_TITLE = "security(ci): npm audit vulnerabilities detected";
|
|
@@ -15,15 +14,40 @@ function runGh(args) {
|
|
|
15
14
|
return result.stdout;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
function readAuditSummary(filePath) {
|
|
18
|
+
const data = JSON.parse(readFileSync(filePath, "utf8"));
|
|
19
|
+
const vulnerabilities = data.metadata?.vulnerabilities ?? {};
|
|
20
|
+
const info = vulnerabilities.info ?? 0;
|
|
21
|
+
const low = vulnerabilities.low ?? 0;
|
|
22
|
+
const moderate = vulnerabilities.moderate ?? 0;
|
|
23
|
+
const high = vulnerabilities.high ?? 0;
|
|
24
|
+
const critical = vulnerabilities.critical ?? 0;
|
|
25
|
+
const total = vulnerabilities.total ?? info + low + moderate + high + critical;
|
|
26
|
+
return { total, critical, high, moderate, low, info };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildBody(summary) {
|
|
30
|
+
const runUrl =
|
|
31
|
+
process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
|
|
32
|
+
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
33
|
+
: "(run URL unavailable)";
|
|
34
|
+
const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 12) : "unknown";
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
"## npm audit detected vulnerabilities",
|
|
38
|
+
"",
|
|
39
|
+
`- Total: ${summary.total}`,
|
|
40
|
+
`- Critical: ${summary.critical}`,
|
|
41
|
+
`- High: ${summary.high}`,
|
|
42
|
+
`- Moderate: ${summary.moderate}`,
|
|
43
|
+
`- Low: ${summary.low}`,
|
|
44
|
+
`- Info: ${summary.info}`,
|
|
45
|
+
"",
|
|
46
|
+
`- Commit: ${sha}`,
|
|
47
|
+
`- Workflow run: ${runUrl}`,
|
|
48
|
+
"",
|
|
49
|
+
"This issue is managed by CI and will be updated on subsequent main-branch runs while vulnerabilities remain.",
|
|
50
|
+
].join("\n");
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
function findExistingIssueNumber() {
|
|
@@ -46,22 +70,13 @@ if (!existsSync(auditFilePath)) {
|
|
|
46
70
|
process.exit(0);
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
73
|
+
const summary = readAuditSummary(auditFilePath);
|
|
74
|
+
if (summary.total === 0) {
|
|
51
75
|
console.log("No vulnerabilities found; skipping issue create/update.");
|
|
52
76
|
process.exit(0);
|
|
53
77
|
}
|
|
54
78
|
|
|
55
|
-
const
|
|
56
|
-
const body = [
|
|
57
|
-
formatMarkdown(parsed).trim(),
|
|
58
|
-
"",
|
|
59
|
-
`- Commit: \`${sha}\``,
|
|
60
|
-
`- Workflow run: ${runUrl()}`,
|
|
61
|
-
"",
|
|
62
|
-
"This issue is managed by CI and will be updated on subsequent main-branch runs while vulnerabilities remain.",
|
|
63
|
-
].join("\n");
|
|
64
|
-
|
|
79
|
+
const body = buildBody(summary);
|
|
65
80
|
const tempFile = path.join(os.tmpdir(), `npm-audit-issue-${Date.now()}.md`);
|
|
66
81
|
writeFileSync(tempFile, body, "utf8");
|
|
67
82
|
|
package/src/bench/bench.mjs
CHANGED
|
@@ -34,8 +34,8 @@ const HISTORY_FILE = join(
|
|
|
34
34
|
"projects/ci-tools/dist/ci/bench/bench-history.json",
|
|
35
35
|
);
|
|
36
36
|
const VITEST_BIN = join(WORKSPACE_ROOT, "node_modules/.bin/vitest");
|
|
37
|
-
const BENCH_FILE = "projects/
|
|
38
|
-
const CONFIG = join(WORKSPACE_ROOT, "vitest.config.ts");
|
|
37
|
+
const BENCH_FILE = "projects/terminal/e2e/build.benchmark.bench.ts";
|
|
38
|
+
const CONFIG = join(WORKSPACE_ROOT, "vitest.e2e.config.ts");
|
|
39
39
|
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".decohere", "logs");
|
|
40
40
|
const MAX_RUNS = 10;
|
|
41
41
|
const command = process.argv[2] ?? "run";
|
|
@@ -326,17 +326,7 @@ const outJson = join(tmpDir, "output.json");
|
|
|
326
326
|
try {
|
|
327
327
|
const result = spawnSync(
|
|
328
328
|
VITEST_BIN,
|
|
329
|
-
[
|
|
330
|
-
"bench",
|
|
331
|
-
"--config",
|
|
332
|
-
CONFIG,
|
|
333
|
-
"--project",
|
|
334
|
-
"e2e",
|
|
335
|
-
"--run",
|
|
336
|
-
"--outputJson",
|
|
337
|
-
outJson,
|
|
338
|
-
BENCH_FILE,
|
|
339
|
-
],
|
|
329
|
+
["bench", "--config", CONFIG, "--run", "--outputJson", outJson, BENCH_FILE],
|
|
340
330
|
{
|
|
341
331
|
cwd: WORKSPACE_ROOT,
|
|
342
332
|
stdio: traceEnabled ? "pipe" : "inherit",
|
package/src/bench/trace.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
const WORKSPACE_ROOT = process.cwd();
|
|
8
8
|
const VITEST_BIN = join(WORKSPACE_ROOT, "node_modules/.bin/vitest");
|
|
9
|
-
const CONFIG = join(WORKSPACE_ROOT, "vitest.config.ts");
|
|
10
|
-
const TRACE_SPEC = "projects/
|
|
9
|
+
const CONFIG = join(WORKSPACE_ROOT, "vitest.e2e.config.ts");
|
|
10
|
+
const TRACE_SPEC = "projects/terminal/e2e/build.trace.e2e.spec.ts";
|
|
11
11
|
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".decohere", "logs");
|
|
12
12
|
|
|
13
13
|
function truthy(value, defaultValue = false) {
|
|
@@ -149,23 +149,19 @@ if (traceLogPath != null) {
|
|
|
149
149
|
mkdirSync(TRACE_LOG_DIR, { recursive: true });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
const result = spawnSync(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
FORGE_TRACE: "1",
|
|
164
|
-
FORGE_TRACE_STRUCTURED: "1",
|
|
165
|
-
LOG_LEVEL: process.env.LOG_LEVEL ?? "trace",
|
|
166
|
-
},
|
|
152
|
+
const result = spawnSync(VITEST_BIN, ["run", "--config", CONFIG, TRACE_SPEC], {
|
|
153
|
+
cwd: WORKSPACE_ROOT,
|
|
154
|
+
stdio: "pipe",
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
maxBuffer: 1024 * 1024 * 200,
|
|
157
|
+
env: {
|
|
158
|
+
...process.env,
|
|
159
|
+
NODE_OPTIONS: "",
|
|
160
|
+
FORGE_TRACE: "1",
|
|
161
|
+
FORGE_TRACE_STRUCTURED: "1",
|
|
162
|
+
LOG_LEVEL: process.env.LOG_LEVEL ?? "trace",
|
|
167
163
|
},
|
|
168
|
-
);
|
|
164
|
+
});
|
|
169
165
|
|
|
170
166
|
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
171
167
|
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { parseLcovByProject } from "../lib/lcov.mjs";
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const options = {
|
|
8
|
+
lcov: ".forge/coverage/lcov.info",
|
|
9
|
+
thresholds: null,
|
|
10
|
+
thresholdsModule: null,
|
|
11
|
+
projectsDir: "projects",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
15
|
+
const arg = argv[index];
|
|
16
|
+
if (arg === "--lcov") {
|
|
17
|
+
options.lcov = argv[index + 1] ?? options.lcov;
|
|
18
|
+
index += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === "--thresholds") {
|
|
22
|
+
options.thresholds = argv[index + 1] ?? options.thresholds;
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === "--thresholds-module") {
|
|
27
|
+
options.thresholdsModule = argv[index + 1] ?? options.thresholdsModule;
|
|
28
|
+
index += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === "--projectsDir") {
|
|
32
|
+
options.projectsDir = argv[index + 1] ?? options.projectsDir;
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function resolveThresholds(options) {
|
|
41
|
+
if (options.thresholdsModule) {
|
|
42
|
+
const moduleUrl = pathToFileURL(resolve(options.thresholdsModule)).href;
|
|
43
|
+
const mod = await import(moduleUrl);
|
|
44
|
+
const thresholds = mod.projectCoverageThresholds;
|
|
45
|
+
if (typeof thresholds !== "object" || thresholds == null) {
|
|
46
|
+
console.error(
|
|
47
|
+
`Module ${options.thresholdsModule} must export a 'projectCoverageThresholds' object.`,
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return thresholds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const path = options.thresholds ?? "coverage.config.json";
|
|
55
|
+
if (!existsSync(path)) {
|
|
56
|
+
console.error(`Thresholds config not found: ${path}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
60
|
+
if (typeof parsed !== "object" || parsed == null) {
|
|
61
|
+
console.error(
|
|
62
|
+
"Thresholds config must be a JSON object mapping project => minimum coverage percentage.",
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatPct(value) {
|
|
70
|
+
return `${value.toFixed(2)}%`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
const options = parseArgs(process.argv.slice(2));
|
|
75
|
+
const lcovPath = resolve(options.lcov);
|
|
76
|
+
const projectsDirPath = resolve(options.projectsDir);
|
|
77
|
+
|
|
78
|
+
if (!existsSync(lcovPath)) {
|
|
79
|
+
console.error(`LCOV file not found: ${lcovPath}`);
|
|
80
|
+
console.error("Run coverage first (example: npm run test:coverage)");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!existsSync(projectsDirPath)) {
|
|
85
|
+
console.error(`Projects directory not found: ${projectsDirPath}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const thresholds = await resolveThresholds(options);
|
|
90
|
+
const projectDirs = readdirSync(projectsDirPath, { withFileTypes: true })
|
|
91
|
+
.filter((entry) => entry.isDirectory())
|
|
92
|
+
.map((entry) => entry.name)
|
|
93
|
+
.sort((left, right) => left.localeCompare(right));
|
|
94
|
+
|
|
95
|
+
const coverageByProject = parseLcovByProject(readFileSync(lcovPath, "utf8"));
|
|
96
|
+
|
|
97
|
+
const missingThresholds = projectDirs.filter(
|
|
98
|
+
(projectName) => thresholds[projectName] == null,
|
|
99
|
+
);
|
|
100
|
+
if (missingThresholds.length > 0) {
|
|
101
|
+
console.error("Missing coverage thresholds for projects:");
|
|
102
|
+
for (const projectName of missingThresholds) console.error(`- ${projectName}`);
|
|
103
|
+
console.error(
|
|
104
|
+
"Update projectCoverageThresholds in vitest.config.ts to include all project directories.",
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const failures = [];
|
|
110
|
+
console.log("Project line coverage thresholds");
|
|
111
|
+
|
|
112
|
+
for (const projectName of projectDirs) {
|
|
113
|
+
const projectCoverage = coverageByProject.get(projectName) ?? {
|
|
114
|
+
totalLines: 0,
|
|
115
|
+
coveredLines: 0,
|
|
116
|
+
};
|
|
117
|
+
const currentPct =
|
|
118
|
+
projectCoverage.totalLines === 0
|
|
119
|
+
? 0
|
|
120
|
+
: (projectCoverage.coveredLines / projectCoverage.totalLines) * 100;
|
|
121
|
+
const minimumPct = Number(thresholds[projectName]);
|
|
122
|
+
const passed = Number.isFinite(minimumPct) && currentPct >= minimumPct;
|
|
123
|
+
|
|
124
|
+
console.log(
|
|
125
|
+
`${passed ? "✅" : "❌"} ${projectName}: ${formatPct(currentPct)} (min ${formatPct(minimumPct)})`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!passed) failures.push({ projectName, currentPct, minimumPct });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (failures.length > 0) {
|
|
132
|
+
console.error("\nCoverage threshold check failed:");
|
|
133
|
+
for (const failure of failures) {
|
|
134
|
+
console.error(
|
|
135
|
+
`- ${failure.projectName}: ${formatPct(failure.currentPct)} is below ${formatPct(failure.minimumPct)}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log("\nAll project coverage thresholds passed.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Clear benchmark history for this publish (workflow_dispatch with clear-history=true)
|
|
4
|
+
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdir } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
|
|
9
|
+
const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(HISTORY_DIR, { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
HISTORY_FILE,
|
|
15
|
+
JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
runs: {},
|
|
18
|
+
},
|
|
19
|
+
null,
|
|
20
|
+
2,
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
console.log("✓ Cleared benchmark history");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Error clearing benchmark history:", error.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
|
10
|
+
const TERMINAL_CLI = process.env.TERMINAL_CLI || "dist/debug/projects/terminal/src/cli.js";
|
|
10
11
|
const DECOHERE_CONFIG = process.env.DECOHERE_CONFIG || ".decohere.js";
|
|
11
12
|
|
|
12
|
-
const baseCmd = `
|
|
13
|
+
const baseCmd = `node "${TERMINAL_CLI}" --log-level trace --config-file "${DECOHERE_CONFIG}" forge dependencies --configuration release sync`;
|
|
13
14
|
|
|
14
15
|
try {
|
|
15
16
|
if (DRY_RUN) {
|
package/src/audit/format.mjs
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared npm/pnpm audit JSON helpers.
|
|
3
|
-
*
|
|
4
|
-
* Supports pnpm's classic `{ advisories }` report and npm audit v2
|
|
5
|
-
* `{ auditReportVersion: 2, vulnerabilities }` (pnpm 9+/10).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
|
|
9
|
-
|
|
10
|
-
function ghsaFromUrl(url) {
|
|
11
|
-
return typeof url === "string" ? (url.match(/GHSA-[a-z0-9-]+/i)?.[0] ?? "") : "";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function fromClassicAdvisories(data) {
|
|
15
|
-
return Object.values(data?.advisories ?? {}).map((raw) => {
|
|
16
|
-
const paths = (raw.findings ?? []).flatMap((f) => f.paths ?? []);
|
|
17
|
-
const url = raw.url ?? "";
|
|
18
|
-
return {
|
|
19
|
-
id: raw.id,
|
|
20
|
-
module: raw.module_name ?? "(unknown)",
|
|
21
|
-
severity: String(raw.severity ?? "info").toLowerCase(),
|
|
22
|
-
title: raw.title ?? "",
|
|
23
|
-
url,
|
|
24
|
-
vulnerable: raw.vulnerable_versions ?? "",
|
|
25
|
-
patched: raw.patched_versions ?? "",
|
|
26
|
-
cves: raw.cves ?? [],
|
|
27
|
-
ghsa: ghsaFromUrl(url),
|
|
28
|
-
paths: paths.slice(0, 3),
|
|
29
|
-
};
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function fromNpmV2(data) {
|
|
34
|
-
const byKey = new Map();
|
|
35
|
-
for (const vuln of Object.values(data?.vulnerabilities ?? {})) {
|
|
36
|
-
const vias = Array.isArray(vuln.via) ? vuln.via : [];
|
|
37
|
-
for (const via of vias) {
|
|
38
|
-
if (via == null || typeof via !== "object") continue;
|
|
39
|
-
const url = via.url ?? "";
|
|
40
|
-
const key = url || `${via.name ?? vuln.name}:${via.title ?? ""}`;
|
|
41
|
-
const existing = byKey.get(key);
|
|
42
|
-
const node = Array.isArray(vuln.nodes) ? vuln.nodes[0] : undefined;
|
|
43
|
-
if (existing) {
|
|
44
|
-
if (node && existing.paths.length < 3 && !existing.paths.includes(node)) {
|
|
45
|
-
existing.paths.push(node);
|
|
46
|
-
}
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
byKey.set(key, {
|
|
50
|
-
id: via.source,
|
|
51
|
-
module: via.name ?? vuln.name ?? "(unknown)",
|
|
52
|
-
severity: String(via.severity ?? vuln.severity ?? "info").toLowerCase(),
|
|
53
|
-
title: via.title ?? "",
|
|
54
|
-
url,
|
|
55
|
-
vulnerable: via.range ?? vuln.range ?? "",
|
|
56
|
-
patched: "",
|
|
57
|
-
cves: [],
|
|
58
|
-
ghsa: ghsaFromUrl(url),
|
|
59
|
-
paths: node ? [node] : [],
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return [...byKey.values()];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function parseAudit(data) {
|
|
67
|
-
const counts = data?.metadata?.vulnerabilities ?? {};
|
|
68
|
-
const info = counts.info ?? 0;
|
|
69
|
-
const low = counts.low ?? 0;
|
|
70
|
-
const moderate = counts.moderate ?? 0;
|
|
71
|
-
const high = counts.high ?? 0;
|
|
72
|
-
const critical = counts.critical ?? 0;
|
|
73
|
-
const total = counts.total ?? info + low + moderate + high + critical;
|
|
74
|
-
|
|
75
|
-
const classic = data?.advisories != null && Object.keys(data.advisories).length > 0;
|
|
76
|
-
const advisories = classic ? fromClassicAdvisories(data) : fromNpmV2(data);
|
|
77
|
-
|
|
78
|
-
advisories.sort((a, b) => {
|
|
79
|
-
const d = SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
|
|
80
|
-
if (d !== 0) return d;
|
|
81
|
-
return String(a.module).localeCompare(String(b.module));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return { total, critical, high, moderate, low, info, advisories };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function formatLog(parsed) {
|
|
88
|
-
const lines = [
|
|
89
|
-
`npm audit: total=${parsed.total} critical=${parsed.critical} high=${parsed.high} moderate=${parsed.moderate} low=${parsed.low} info=${parsed.info}`,
|
|
90
|
-
];
|
|
91
|
-
for (const a of parsed.advisories) {
|
|
92
|
-
const id = a.ghsa || a.cves[0] || a.id;
|
|
93
|
-
lines.push(
|
|
94
|
-
` [${a.severity}] ${a.module} ${id}: ${a.title} (vulnerable ${a.vulnerable}; patched ${a.patched})`,
|
|
95
|
-
);
|
|
96
|
-
if (a.url) lines.push(` ${a.url}`);
|
|
97
|
-
if (a.paths[0]) lines.push(` ${a.paths[0]}`);
|
|
98
|
-
}
|
|
99
|
-
return `${lines.join("\n")}\n`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function formatMarkdown(parsed) {
|
|
103
|
-
const lines = [
|
|
104
|
-
"## npm audit",
|
|
105
|
-
"",
|
|
106
|
-
"| Severity | Count |",
|
|
107
|
-
"|---|---|",
|
|
108
|
-
`| Critical | ${parsed.critical} |`,
|
|
109
|
-
`| High | ${parsed.high} |`,
|
|
110
|
-
`| Moderate | ${parsed.moderate} |`,
|
|
111
|
-
`| Low | ${parsed.low} |`,
|
|
112
|
-
`| Info | ${parsed.info} |`,
|
|
113
|
-
`| **Total** | **${parsed.total}** |`,
|
|
114
|
-
"",
|
|
115
|
-
];
|
|
116
|
-
if (parsed.advisories.length === 0) {
|
|
117
|
-
lines.push("No advisories.");
|
|
118
|
-
return `${lines.join("\n")}\n`;
|
|
119
|
-
}
|
|
120
|
-
lines.push("### Advisories", "");
|
|
121
|
-
for (const a of parsed.advisories) {
|
|
122
|
-
const id = a.ghsa || a.cves.join(", ") || a.id;
|
|
123
|
-
const link = a.url ? `[${id}](${a.url})` : id;
|
|
124
|
-
lines.push(`- **${a.severity}** \`${a.module}\` ${link} — ${a.title}`);
|
|
125
|
-
lines.push(` - vulnerable: \`${a.vulnerable}\`; patched: \`${a.patched}\``);
|
|
126
|
-
if (a.paths[0]) lines.push(` - ${a.paths[0]}`);
|
|
127
|
-
}
|
|
128
|
-
return `${lines.join("\n")}\n`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function highCritical(parsed) {
|
|
132
|
-
return parsed.advisories.filter((a) => a.severity === "high" || a.severity === "critical");
|
|
133
|
-
}
|