@decohere/ci-tools 0.0.1-dev.97 → 0.0.1-dev.99
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 +1 -2
- package/src/bench/bench.mjs +12 -2
- package/src/bench/trace.mjs +17 -13
- package/src/coverage/enforce-thresholds.mjs +0 -144
- package/src/pages/clear-bench-history.mjs +0 -27
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.99",
|
|
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,7 +18,6 @@
|
|
|
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",
|
|
22
21
|
"./badges/ci-status": "./src/badges/ci-status.mjs",
|
|
23
22
|
"./pages/artifact": "./src/pages/artifact.mjs",
|
|
24
23
|
"./pages/smoke": "./src/pages/smoke.mjs",
|
package/src/bench/bench.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const HISTORY_FILE = join(
|
|
|
35
35
|
);
|
|
36
36
|
const VITEST_BIN = join(WORKSPACE_ROOT, "node_modules/.bin/vitest");
|
|
37
37
|
const BENCH_FILE = "projects/cli/e2e/build.benchmark.bench.ts";
|
|
38
|
-
const CONFIG = join(WORKSPACE_ROOT, "vitest.
|
|
38
|
+
const CONFIG = join(WORKSPACE_ROOT, "vitest.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,7 +326,17 @@ const outJson = join(tmpDir, "output.json");
|
|
|
326
326
|
try {
|
|
327
327
|
const result = spawnSync(
|
|
328
328
|
VITEST_BIN,
|
|
329
|
-
[
|
|
329
|
+
[
|
|
330
|
+
"bench",
|
|
331
|
+
"--config",
|
|
332
|
+
CONFIG,
|
|
333
|
+
"--project",
|
|
334
|
+
"e2e",
|
|
335
|
+
"--run",
|
|
336
|
+
"--outputJson",
|
|
337
|
+
outJson,
|
|
338
|
+
BENCH_FILE,
|
|
339
|
+
],
|
|
330
340
|
{
|
|
331
341
|
cwd: WORKSPACE_ROOT,
|
|
332
342
|
stdio: traceEnabled ? "pipe" : "inherit",
|
package/src/bench/trace.mjs
CHANGED
|
@@ -6,7 +6,7 @@ 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.
|
|
9
|
+
const CONFIG = join(WORKSPACE_ROOT, "vitest.config.ts");
|
|
10
10
|
const TRACE_SPEC = "projects/cli/e2e/build.trace.e2e.spec.ts";
|
|
11
11
|
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".decohere", "logs");
|
|
12
12
|
|
|
@@ -149,19 +149,23 @@ 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
|
-
|
|
152
|
+
const result = spawnSync(
|
|
153
|
+
VITEST_BIN,
|
|
154
|
+
["run", "--config", CONFIG, "--project", "e2e", TRACE_SPEC],
|
|
155
|
+
{
|
|
156
|
+
cwd: WORKSPACE_ROOT,
|
|
157
|
+
stdio: "pipe",
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
maxBuffer: 1024 * 1024 * 200,
|
|
160
|
+
env: {
|
|
161
|
+
...process.env,
|
|
162
|
+
NODE_OPTIONS: "",
|
|
163
|
+
FORGE_TRACE: "1",
|
|
164
|
+
FORGE_TRACE_STRUCTURED: "1",
|
|
165
|
+
LOG_LEVEL: process.env.LOG_LEVEL ?? "trace",
|
|
166
|
+
},
|
|
163
167
|
},
|
|
164
|
-
|
|
168
|
+
);
|
|
165
169
|
|
|
166
170
|
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
167
171
|
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
@@ -1,144 +0,0 @@
|
|
|
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();
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|