@decohere/ci-tools 0.0.1-dev.119 → 0.0.1-dev.12
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 +74 -25
- package/src/bench/bench.mjs +368 -134
- package/src/bench/trace.mjs +14 -18
- package/src/coverage/enforce-thresholds.mjs +144 -0
- package/src/pages/artifact.mjs +183 -38
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/pages/keep-prior-bench-badge.mjs +38 -0
- package/src/pages/restore-bench-history.mjs +28 -15
- package/src/release/npm-links.mjs +13 -6
- package/src/release/sync-dependencies.mjs +2 -1
- package/tests/lib/lcov.test.mjs +4 -4
- package/src/audit/format.mjs +0 -133
- package/src/bench/file-regressions.mjs +0 -76
- package/src/bench/history.mjs +0 -248
- package/src/bench/nightly.mjs +0 -118
- package/src/bench/regression-issue.mjs +0 -44
- package/src/github/credential-nudge.mjs +0 -245
- package/src/github/oidc-bootstrap-nudge.mjs +0 -210
- package/src/github/publish-verify.mjs +0 -206
- package/src/github/smoke-create-launcher.mjs +0 -70
- package/src/lib/github-issue.mjs +0 -75
- package/src/lib/workspace-packages.mjs +0 -30
- package/src/pages/bench-page.mjs +0 -323
- package/src/pages/restore-badges.mjs +0 -58
- package/tests/audit/format.test.mjs +0 -115
- package/tests/bench/history.test.mjs +0 -279
- package/tests/bench/regression-issue.test.mjs +0 -42
- package/tests/github/credential-nudge.test.mjs +0 -116
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
- package/tests/github/publish-verify.test.mjs +0 -115
- package/tests/github/smoke-create-launcher.test.mjs +0 -71
- package/tests/lib/github-issue.test.mjs +0 -85
- package/tests/pages/bench-page.test.mjs +0 -123
|
@@ -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();
|
package/src/pages/artifact.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { normalize } from "../bench/history.mjs";
|
|
5
|
-
import { buildBenchmarkHtml, buildStylesCss } from "./bench-page.mjs";
|
|
6
3
|
|
|
7
4
|
const branch = process.env.GITHUB_REF_NAME ?? "local";
|
|
8
5
|
const sha = process.env.GITHUB_SHA ?? "local";
|
|
@@ -11,53 +8,201 @@ const outputDir = process.env.PAGES_OUTPUT_DIR ?? "site";
|
|
|
11
8
|
const badgesDir = "projects/ci-tools/dist/ci/badges";
|
|
12
9
|
const benchHistoryPath = "projects/ci-tools/dist/ci/bench/bench-history.json";
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
const numericFields = ["hz", "mean", "p99", "rme", "sampleCount"];
|
|
12
|
+
|
|
13
|
+
function fmtMs(ms) {
|
|
14
|
+
if (typeof ms !== "number" || Number.isNaN(ms)) return "-";
|
|
15
|
+
if (ms >= 100) return `${Math.round(ms)} ms`;
|
|
16
|
+
return `${ms.toFixed(1)} ms`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fmtHz(hz) {
|
|
20
|
+
if (typeof hz !== "number" || Number.isNaN(hz)) return "-";
|
|
21
|
+
return `${hz.toFixed(2)}/s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function averageBenchmarks(runs) {
|
|
25
|
+
const first = runs[0]?.vitestOutput;
|
|
26
|
+
if (first == null) return [];
|
|
27
|
+
|
|
28
|
+
const latestBenchmarks = first.files
|
|
29
|
+
.flatMap((file) => file.groups)
|
|
30
|
+
.flatMap((group) => group.benchmarks);
|
|
31
|
+
|
|
32
|
+
return latestBenchmarks.map((benchmark) => {
|
|
33
|
+
const peers = runs
|
|
34
|
+
.flatMap((run) => run.vitestOutput?.files ?? [])
|
|
35
|
+
.flatMap((file) => file.groups ?? [])
|
|
36
|
+
.flatMap((group) => group.benchmarks ?? [])
|
|
37
|
+
.filter((candidate) => candidate.name === benchmark.name);
|
|
38
|
+
|
|
39
|
+
const averaged = { name: benchmark.name };
|
|
40
|
+
for (const field of numericFields) {
|
|
41
|
+
const values = peers
|
|
42
|
+
.map((peer) => peer[field])
|
|
43
|
+
.filter((value) => typeof value === "number");
|
|
44
|
+
if (values.length > 0) {
|
|
45
|
+
averaged[field] = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return averaged;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildBenchmarkHtml(benchHistory) {
|
|
54
|
+
const contexts = Object.entries(benchHistory.runs ?? {})
|
|
55
|
+
.filter(([, entry]) => Array.isArray(entry.runs) && entry.runs.length > 0)
|
|
56
|
+
.sort((a, b) => new Date(b[1].runs[0].date) - new Date(a[1].runs[0].date));
|
|
57
|
+
|
|
58
|
+
if (contexts.length === 0) {
|
|
59
|
+
return `<!doctype html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="utf-8" />
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
64
|
+
<title>Ark - Benchmarks</title>
|
|
65
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
<main class="container">
|
|
69
|
+
<header class="hero">
|
|
70
|
+
<h1>Benchmarks</h1>
|
|
71
|
+
<p class="note">No benchmark history is available yet.</p>
|
|
72
|
+
</header>
|
|
73
|
+
</main>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (const [, entry] of contexts) {
|
|
81
|
+
const benchmarks = averageBenchmarks(entry.runs);
|
|
82
|
+
const ctx = entry.context;
|
|
83
|
+
const environment = `${ctx.benchEnv ?? "unknown"}-${ctx.os.arch}`;
|
|
84
|
+
for (const benchmark of benchmarks) {
|
|
85
|
+
rows.push(`<tr>
|
|
86
|
+
<td>${environment}</td>
|
|
87
|
+
<td><code>${benchmark.name}</code></td>
|
|
88
|
+
<td>${fmtMs(benchmark.mean)}</td>
|
|
89
|
+
<td>${typeof benchmark.rme === "number" ? `±${benchmark.rme.toFixed(1)}%` : "-"}</td>
|
|
90
|
+
<td>${fmtMs(benchmark.p99)}</td>
|
|
91
|
+
<td>${fmtHz(benchmark.hz)}</td>
|
|
92
|
+
<td>${typeof benchmark.sampleCount === "number" ? Math.round(benchmark.sampleCount) : "-"}</td>
|
|
93
|
+
</tr>`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const details = contexts
|
|
98
|
+
.map(([, entry]) => {
|
|
99
|
+
const ctx = entry.context;
|
|
100
|
+
return `<li><strong>${ctx.benchEnv ?? "unknown"}-${ctx.os.arch}</strong> - ${ctx.os.platform} - ${ctx.cpu.model} x ${ctx.cpu.count} - ${ctx.memoryGb} GB - Node ${ctx.node} - rolling mean of ${entry.runs.length} run(s)</li>`;
|
|
101
|
+
})
|
|
102
|
+
.join("\n");
|
|
103
|
+
|
|
104
|
+
const updatedDate = contexts[0][1].runs[0].date.slice(0, 10);
|
|
105
|
+
|
|
106
|
+
return `<!doctype html>
|
|
107
|
+
<html lang="en">
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="utf-8" />
|
|
110
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
111
|
+
<title>Ark - Benchmarks</title>
|
|
112
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<main class="container">
|
|
116
|
+
<div class="hero">
|
|
117
|
+
<h1>Benchmarks</h1>
|
|
118
|
+
<p class="note">Updated ${updatedDate} · Branch: ${branch} · Commit: ${sha.slice(0, 12)}</p>
|
|
119
|
+
</div>
|
|
120
|
+
<section class="section">
|
|
121
|
+
<h2>Performance Table</h2>
|
|
122
|
+
<table>
|
|
123
|
+
<thead>
|
|
124
|
+
<tr>
|
|
125
|
+
<th>Environment</th>
|
|
126
|
+
<th>Benchmark</th>
|
|
127
|
+
<th>Mean</th>
|
|
128
|
+
<th>±RME</th>
|
|
129
|
+
<th>P99</th>
|
|
130
|
+
<th>Ops/s</th>
|
|
131
|
+
<th>Runs</th>
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
${rows.join("\n")}
|
|
136
|
+
</tbody>
|
|
137
|
+
</table>
|
|
138
|
+
</section>
|
|
139
|
+
<section class="section">
|
|
140
|
+
<h2>Environment Notes</h2>
|
|
141
|
+
<ul>
|
|
142
|
+
${details}
|
|
143
|
+
</ul>
|
|
144
|
+
</section>
|
|
145
|
+
</main>
|
|
146
|
+
</body>
|
|
147
|
+
</html>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildStylesCss() {
|
|
152
|
+
return `:root {
|
|
153
|
+
--bg: #f4f7f3;
|
|
154
|
+
--surface: #ffffff;
|
|
155
|
+
--text: #132b24;
|
|
156
|
+
--muted: #4f6f65;
|
|
157
|
+
--accent: #0f9d72;
|
|
158
|
+
--accent-strong: #0d7f5c;
|
|
159
|
+
--border: #d6e3dd;
|
|
160
|
+
--shadow: 0 14px 40px rgba(12, 58, 45, 0.08);
|
|
161
|
+
}
|
|
162
|
+
* { box-sizing: border-box; }
|
|
163
|
+
body { margin: 0; font-family: "Segoe UI", "Helvetica Neue", sans-serif; color: var(--text); background: radial-gradient(circle at top right, #dbefe7, var(--bg) 40%); }
|
|
164
|
+
a { color: var(--accent-strong); }
|
|
165
|
+
.container { max-width: 1100px; margin: 0 auto; padding: 28px 18px 60px; }
|
|
166
|
+
.hero { background: linear-gradient(145deg, #183a31, #0e7f5c); color: #f3fff8; padding: 26px; border-radius: 18px; box-shadow: var(--shadow); }
|
|
167
|
+
.hero h1 { margin: 6px 0 10px; }
|
|
168
|
+
.note { color: #d8f5eb; margin: 0; }
|
|
169
|
+
.section { margin-top: 26px; }
|
|
170
|
+
table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: 12px; overflow: hidden; }
|
|
171
|
+
th, td { border-bottom: 1px solid var(--border); padding: 9px 10px; text-align: left; }
|
|
172
|
+
th { color: var(--muted); font-weight: 600; background: #f3faf7; }
|
|
173
|
+
td code { color: var(--accent-strong); }
|
|
174
|
+
@media (max-width: 700px) { .hero { padding: 20px; } .container { padding: 18px 12px 40px; } }
|
|
175
|
+
`;
|
|
27
176
|
}
|
|
28
177
|
|
|
29
178
|
await rm(outputDir, { recursive: true, force: true });
|
|
30
|
-
await mkdir(
|
|
179
|
+
await mkdir(outputDir, { recursive: true });
|
|
31
180
|
|
|
32
181
|
await writeFile(path.join(outputDir, "styles.css"), buildStylesCss());
|
|
33
182
|
|
|
34
183
|
// index.html redirects to benchmark.html — no separate dashboard needed
|
|
35
184
|
await writeFile(
|
|
36
185
|
path.join(outputDir, "index.html"),
|
|
37
|
-
`<!doctype html><html><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=./benchmark.html"/><title>
|
|
186
|
+
`<!doctype html><html><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=./benchmark.html"/><title>Ark Pages</title></head><body><p><a href="./benchmark.html">Benchmarks</a></p></body></html>\n`,
|
|
38
187
|
);
|
|
39
188
|
|
|
40
|
-
|
|
41
|
-
const benchHistory =
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
await
|
|
45
|
-
path.join(outputDir, "benchmark.html"),
|
|
46
|
-
buildBenchmarkHtml(benchHistory, { branch, sha }),
|
|
47
|
-
);
|
|
48
|
-
await writeFile(
|
|
49
|
-
path.join(outputDir, "data", "bench-history.json"),
|
|
50
|
-
`${JSON.stringify(benchHistory, null, 2)}\n`,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Badges are generated by earlier steps (CI status, coverage) or restored from the live site.
|
|
54
|
-
if (existsSync(badgesDir)) {
|
|
55
|
-
await cp(badgesDir, path.join(outputDir, "badges"), { recursive: true });
|
|
56
|
-
const badges = await listBadges(badgesDir);
|
|
189
|
+
try {
|
|
190
|
+
const benchHistory = JSON.parse(await readFile(benchHistoryPath, "utf8"));
|
|
191
|
+
await writeFile(path.join(outputDir, "benchmark.html"), buildBenchmarkHtml(benchHistory));
|
|
192
|
+
const benchmarkDataDir = path.join(outputDir, "data");
|
|
193
|
+
await mkdir(benchmarkDataDir, { recursive: true });
|
|
57
194
|
await writeFile(
|
|
58
|
-
path.join(
|
|
59
|
-
`${JSON.stringify(
|
|
195
|
+
path.join(benchmarkDataDir, "bench-history.json"),
|
|
196
|
+
`${JSON.stringify(benchHistory, null, 2)}\n`,
|
|
60
197
|
);
|
|
198
|
+
} catch {
|
|
199
|
+
await writeFile(path.join(outputDir, "benchmark.html"), buildBenchmarkHtml({ runs: {} }));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await cp(badgesDir, path.join(outputDir, "badges"), { recursive: true });
|
|
204
|
+
} catch {
|
|
205
|
+
// Badges may not exist yet for some runs.
|
|
61
206
|
}
|
|
62
207
|
|
|
63
208
|
await writeFile(path.join(outputDir, ".nojekyll"), "");
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Keep prior bench badge if no code changes; create fallback if none exists
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const BADGES_DIR = "projects/ci-tools/dist/ci/badges";
|
|
10
|
+
const BADGE_FILE = `${BADGES_DIR}/bench.svg`;
|
|
11
|
+
const PAGES_URL = "https://typesmiths.github.io/decohere/badges/bench.svg";
|
|
12
|
+
|
|
13
|
+
const FALLBACK_BADGE = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20" role="img" aria-label="bench: unavailable">
|
|
14
|
+
<title>bench: unavailable</title>
|
|
15
|
+
<rect width="120" height="20" fill="#555"/>
|
|
16
|
+
<rect x="46" width="74" height="20" fill="#9f9f9f"/>
|
|
17
|
+
<text x="24" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">bench</text>
|
|
18
|
+
<text x="83" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">unavailable</text>
|
|
19
|
+
</svg>`;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(BADGE_FILE)) {
|
|
23
|
+
await mkdir(BADGES_DIR, { recursive: true });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
execSync(`curl -fsSL "${PAGES_URL}" -o "${BADGE_FILE}"`, {
|
|
27
|
+
stdio: "pipe",
|
|
28
|
+
});
|
|
29
|
+
console.log("✓ Restored prior bench badge from Pages");
|
|
30
|
+
} catch {
|
|
31
|
+
console.warn("::warning::No prior bench badge found on Pages; creating fallback badge.");
|
|
32
|
+
writeFileSync(BADGE_FILE, FALLBACK_BADGE);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error keeping prior bench badge:", error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Restore benchmark history from Pages
|
|
4
|
-
// empty history: any other failure stops the job, so a network blip can't wipe the trend on
|
|
5
|
-
// the next save.
|
|
3
|
+
// Restore benchmark history from Pages, or initialize empty if not found
|
|
6
4
|
|
|
7
|
-
import {
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { writeFileSync } from "node:fs";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
8
|
|
|
9
9
|
const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
|
|
10
10
|
const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
|
|
11
|
-
const PAGES_URL =
|
|
11
|
+
const PAGES_URL = "https://typesmiths.github.io/decohere/data/bench-history.json";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(HISTORY_DIR, { recursive: true });
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
execSync(`curl -fsSL "${PAGES_URL}" -o "${HISTORY_FILE}"`, {
|
|
18
|
+
stdio: "pipe",
|
|
19
|
+
});
|
|
20
|
+
console.log("✓ Restored benchmark history from Pages");
|
|
21
|
+
} catch {
|
|
22
|
+
console.log("No benchmark history on Pages; starting from empty history");
|
|
23
|
+
writeFileSync(
|
|
24
|
+
HISTORY_FILE,
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
runs: {},
|
|
28
|
+
},
|
|
29
|
+
null,
|
|
30
|
+
2,
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Error restoring benchmark history:", error.message);
|
|
20
36
|
process.exit(1);
|
|
21
|
-
} else {
|
|
22
|
-
await writeFile(HISTORY_FILE, await response.text());
|
|
23
|
-
console.log("✓ Restored benchmark history from Pages");
|
|
24
37
|
}
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* Outputs markdown formatted links for GitHub release notes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { listProjectDirs } from "../lib/workspace-packages.mjs";
|
|
10
9
|
|
|
11
10
|
const VERSION = process.argv[2] || process.env.VERSION;
|
|
12
11
|
|
|
@@ -16,12 +15,20 @@ if (!VERSION) {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
let links = "## Published Packages\n\n";
|
|
18
|
+
const projectsDir = join(process.cwd(), "projects");
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const projects = readdirSync(projectsDir);
|
|
22
|
+
|
|
23
|
+
for (const project of projects) {
|
|
24
|
+
const pkgPath = join(projectsDir, project, "package.json");
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
27
|
+
if (pkg.name?.startsWith("@decohere/")) {
|
|
28
|
+
links += `- [\`${pkg.name}@${VERSION}\`](https://www.npmjs.com/package/${pkg.name}/v/${VERSION})\n`;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Skip projects without package.json
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -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/tests/lib/lcov.test.mjs
CHANGED
|
@@ -13,7 +13,7 @@ SF:projects/forge/src/build/index.ts
|
|
|
13
13
|
LF:20
|
|
14
14
|
LH:15
|
|
15
15
|
end_of_record
|
|
16
|
-
SF:projects/
|
|
16
|
+
SF:projects/nexus/src/index.ts
|
|
17
17
|
LF:5
|
|
18
18
|
LH:5
|
|
19
19
|
end_of_record
|
|
@@ -46,9 +46,9 @@ describe("parseLcovByProject", () => {
|
|
|
46
46
|
expect(result.get("forge")).toBeDefined();
|
|
47
47
|
expect(result.get("forge").totalLines).toBe(30);
|
|
48
48
|
expect(result.get("forge").coveredLines).toBe(24);
|
|
49
|
-
expect(result.get("
|
|
50
|
-
expect(result.get("
|
|
51
|
-
expect(result.get("
|
|
49
|
+
expect(result.get("nexus")).toBeDefined();
|
|
50
|
+
expect(result.get("nexus").totalLines).toBe(5);
|
|
51
|
+
expect(result.get("nexus").coveredLines).toBe(5);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("ignores records with LF:0", () => {
|