@kb-labs/qa-core 0.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.md +120 -0
- package/dist/baseline/index.d.ts +27 -0
- package/dist/baseline/index.js +762 -0
- package/dist/baseline/index.js.map +1 -0
- package/dist/categories/index.d.ts +13 -0
- package/dist/categories/index.js +39 -0
- package/dist/categories/index.js.map +1 -0
- package/dist/history/index.d.ts +47 -0
- package/dist/history/index.js +324 -0
- package/dist/history/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1550 -0
- package/dist/index.js.map +1 -0
- package/dist/last-run-store-CQ_6ai40.d.ts +78 -0
- package/dist/report/index.d.ts +51 -0
- package/dist/report/index.js +423 -0
- package/dist/report/index.js.map +1 -0
- package/dist/runner/index.d.ts +43 -0
- package/dist/runner/index.js +713 -0
- package/dist/runner/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative, dirname, resolve } from 'path';
|
|
3
|
+
import { execSync, spawnSync } from 'child_process';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { PATHS } from '@kb-labs/qa-contracts';
|
|
6
|
+
|
|
7
|
+
// src/runner/workspace.ts
|
|
8
|
+
function getSubmoduleInfo(repoDir, repoName) {
|
|
9
|
+
const gitDir = join(repoDir, ".git");
|
|
10
|
+
if (!existsSync(gitDir)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const commit = execSync("git rev-parse --short HEAD", {
|
|
15
|
+
cwd: repoDir,
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
timeout: 5e3,
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
19
|
+
}).trim();
|
|
20
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
21
|
+
cwd: repoDir,
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
timeout: 5e3,
|
|
24
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
25
|
+
}).trim();
|
|
26
|
+
const message = execSync("git log -1 --format=%s", {
|
|
27
|
+
cwd: repoDir,
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
timeout: 5e3,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
31
|
+
}).trim();
|
|
32
|
+
const statusOutput = execSync("git status --porcelain", {
|
|
33
|
+
cwd: repoDir,
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
timeout: 5e3,
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
37
|
+
}).trim();
|
|
38
|
+
return {
|
|
39
|
+
name: repoName,
|
|
40
|
+
commit,
|
|
41
|
+
branch,
|
|
42
|
+
dirty: statusOutput.length > 0,
|
|
43
|
+
message
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function collectSubmoduleInfo(rootDir, repos) {
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const repo of repos) {
|
|
52
|
+
const repoDir = join(rootDir, repo);
|
|
53
|
+
const info = getSubmoduleInfo(repoDir, repo);
|
|
54
|
+
if (info) {
|
|
55
|
+
result[repo] = info;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/runner/workspace.ts
|
|
62
|
+
function hasWorkspace(dir) {
|
|
63
|
+
return existsSync(join(dir, "pnpm-workspace.yaml"));
|
|
64
|
+
}
|
|
65
|
+
function isDir(p) {
|
|
66
|
+
return existsSync(p) && statSync(p).isDirectory();
|
|
67
|
+
}
|
|
68
|
+
function buildCandidatesFromConfig(rootDir, paths) {
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const pattern of paths) {
|
|
71
|
+
const parts = pattern.split("/");
|
|
72
|
+
if (parts.length === 2 && parts[1] === "*" && parts[0]) {
|
|
73
|
+
const categoryDir = join(rootDir, parts[0]);
|
|
74
|
+
if (!isDir(categoryDir)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
for (const sub of readdirSync(categoryDir)) {
|
|
79
|
+
if (sub.startsWith(".") || sub === "node_modules") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const subPath = join(categoryDir, sub);
|
|
83
|
+
if (isDir(subPath) && hasWorkspace(subPath)) {
|
|
84
|
+
candidates.push(subPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const exactPath = join(rootDir, pattern);
|
|
91
|
+
if (isDir(exactPath) && hasWorkspace(exactPath)) {
|
|
92
|
+
candidates.push(exactPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return candidates;
|
|
97
|
+
}
|
|
98
|
+
function buildCandidatesAutoScan(rootDir) {
|
|
99
|
+
const candidates = [];
|
|
100
|
+
for (const entry of readdirSync(rootDir)) {
|
|
101
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const entryPath = join(rootDir, entry);
|
|
105
|
+
if (!isDir(entryPath)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (hasWorkspace(entryPath)) {
|
|
109
|
+
candidates.push(entryPath);
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
for (const sub of readdirSync(entryPath)) {
|
|
113
|
+
if (sub.startsWith(".") || sub === "node_modules") {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const subPath = join(entryPath, sub);
|
|
117
|
+
if (isDir(subPath) && hasWorkspace(subPath)) {
|
|
118
|
+
candidates.push(subPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return candidates;
|
|
126
|
+
}
|
|
127
|
+
function scanSubDir(parentDir, entryPath, repoName, rootDir, submodule, packages) {
|
|
128
|
+
if (!isDir(parentDir)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const pkgDir of readdirSync(parentDir)) {
|
|
132
|
+
const pkgPath = join(parentDir, pkgDir);
|
|
133
|
+
const pkgJsonPath = join(pkgPath, "package.json");
|
|
134
|
+
if (!existsSync(pkgJsonPath)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
139
|
+
packages.push({
|
|
140
|
+
name: pkgJson.name || pkgDir,
|
|
141
|
+
dir: pkgPath,
|
|
142
|
+
relativePath: relative(rootDir, pkgPath),
|
|
143
|
+
repo: repoName,
|
|
144
|
+
submodule
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function getWorkspacePackages(rootDir, filter, packagesConfig) {
|
|
151
|
+
const packages = [];
|
|
152
|
+
const submoduleCache = /* @__PURE__ */ new Map();
|
|
153
|
+
function getSubmoduleCached(entryPath, repoName) {
|
|
154
|
+
if (!submoduleCache.has(repoName)) {
|
|
155
|
+
submoduleCache.set(repoName, getSubmoduleInfo(entryPath, repoName));
|
|
156
|
+
}
|
|
157
|
+
return submoduleCache.get(repoName) ?? void 0;
|
|
158
|
+
}
|
|
159
|
+
const candidates = packagesConfig?.paths && packagesConfig.paths.length > 0 ? buildCandidatesFromConfig(rootDir, packagesConfig.paths) : buildCandidatesAutoScan(rootDir);
|
|
160
|
+
for (const entryPath of candidates) {
|
|
161
|
+
const repoName = relative(rootDir, entryPath);
|
|
162
|
+
const submodule = getSubmoduleCached(entryPath, repoName);
|
|
163
|
+
scanSubDir(join(entryPath, "packages"), entryPath, repoName, rootDir, submodule, packages);
|
|
164
|
+
scanSubDir(join(entryPath, "apps"), entryPath, repoName, rootDir, submodule, packages);
|
|
165
|
+
}
|
|
166
|
+
let filtered = packages;
|
|
167
|
+
if (packagesConfig?.include && packagesConfig.include.length > 0) {
|
|
168
|
+
filtered = filtered.filter(
|
|
169
|
+
(pkg) => packagesConfig.include.some((pattern) => matchesPattern(pkg.name, pkg.repo, pattern))
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (packagesConfig?.exclude && packagesConfig.exclude.length > 0) {
|
|
173
|
+
filtered = filtered.filter(
|
|
174
|
+
(pkg) => !packagesConfig.exclude.some((pattern) => matchesPattern(pkg.name, pkg.repo, pattern))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!filter) {
|
|
178
|
+
return filtered;
|
|
179
|
+
}
|
|
180
|
+
return filtered.filter((pkg) => {
|
|
181
|
+
if (filter.package && !pkg.name.includes(filter.package)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (filter.repo && pkg.repo !== filter.repo) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (filter.scope) {
|
|
188
|
+
const scope = filter.scope.startsWith("@") ? filter.scope : `@${filter.scope}`;
|
|
189
|
+
if (!pkg.name.startsWith(scope)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function matchesPattern(name, repo, pattern) {
|
|
197
|
+
if (pattern.endsWith("/*")) {
|
|
198
|
+
const prefix = pattern.slice(0, -2);
|
|
199
|
+
return repo === prefix || repo.startsWith(prefix + "/");
|
|
200
|
+
}
|
|
201
|
+
if (pattern.endsWith("*")) {
|
|
202
|
+
return name.startsWith(pattern.slice(0, -1));
|
|
203
|
+
}
|
|
204
|
+
return name === pattern || repo === pattern;
|
|
205
|
+
}
|
|
206
|
+
function readWorkspaceDeps(pkgDir, workspaceNames) {
|
|
207
|
+
try {
|
|
208
|
+
const raw = readFileSync(join(pkgDir, "package.json"), "utf-8");
|
|
209
|
+
const pkgJson = JSON.parse(raw);
|
|
210
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
211
|
+
return Object.keys(allDeps).filter((dep) => workspaceNames.has(dep));
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function buildDepGraph(packages) {
|
|
217
|
+
const nameMap = /* @__PURE__ */ new Map();
|
|
218
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const pkg of packages) {
|
|
220
|
+
nameMap.set(pkg.name, pkg);
|
|
221
|
+
workspaceNames.add(pkg.name);
|
|
222
|
+
}
|
|
223
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
224
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const pkg of packages) {
|
|
226
|
+
inDegree.set(pkg.name, 0);
|
|
227
|
+
dependents.set(pkg.name, []);
|
|
228
|
+
}
|
|
229
|
+
for (const pkg of packages) {
|
|
230
|
+
const deps = readWorkspaceDeps(pkg.dir, workspaceNames);
|
|
231
|
+
inDegree.set(pkg.name, deps.length);
|
|
232
|
+
for (const dep of deps) {
|
|
233
|
+
dependents.get(dep).push(pkg.name);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { nameMap, inDegree, dependents };
|
|
237
|
+
}
|
|
238
|
+
function computeBuildLayers(packages) {
|
|
239
|
+
if (packages.length === 0) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const { nameMap, inDegree, dependents } = buildDepGraph(packages);
|
|
243
|
+
const layers = [];
|
|
244
|
+
const remaining = new Set(packages.map((p) => p.name));
|
|
245
|
+
while (remaining.size > 0) {
|
|
246
|
+
const layerNames = [];
|
|
247
|
+
for (const name of remaining) {
|
|
248
|
+
if ((inDegree.get(name) ?? 0) === 0) {
|
|
249
|
+
layerNames.push(name);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (layerNames.length === 0) {
|
|
253
|
+
const circular = [...remaining].map((n) => nameMap.get(n)).filter(Boolean);
|
|
254
|
+
layers.push({ index: layers.length, packages: circular });
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
layerNames.sort();
|
|
258
|
+
layers.push({ index: layers.length, packages: layerNames.map((n) => nameMap.get(n)) });
|
|
259
|
+
for (const name of layerNames) {
|
|
260
|
+
remaining.delete(name);
|
|
261
|
+
for (const dependent of dependents.get(name) ?? []) {
|
|
262
|
+
inDegree.set(dependent, (inDegree.get(dependent) ?? 0) - 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return layers;
|
|
267
|
+
}
|
|
268
|
+
function sortByBuildLayers(packages) {
|
|
269
|
+
return computeBuildLayers(packages).flatMap((l) => l.packages);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/runner/custom-check-runner.ts
|
|
273
|
+
var ID_MAP = {
|
|
274
|
+
build: "build",
|
|
275
|
+
lint: "lint",
|
|
276
|
+
typecheck: "typeCheck",
|
|
277
|
+
"type-check": "typeCheck",
|
|
278
|
+
test: "test",
|
|
279
|
+
tests: "test"
|
|
280
|
+
};
|
|
281
|
+
function emptyResult() {
|
|
282
|
+
return { passed: [], failed: [], skipped: [], errors: {} };
|
|
283
|
+
}
|
|
284
|
+
function runCommand(command, args, cwd, timeoutMs) {
|
|
285
|
+
try {
|
|
286
|
+
const result = spawnSync(command, args, { cwd, timeout: timeoutMs, encoding: "utf-8", shell: false });
|
|
287
|
+
const stdout = result.stdout ?? "";
|
|
288
|
+
const stderr = result.stderr ?? "";
|
|
289
|
+
const exitCode = result.status ?? 1;
|
|
290
|
+
return { ok: exitCode === 0, stdout, stderr, exitCode };
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { ok: false, stdout: "", stderr: e.message ?? String(e), exitCode: 1 };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function evaluate(check, stdout, stderr, exitCode) {
|
|
296
|
+
if ((check.parser ?? "exitcode") === "json") {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(stdout);
|
|
299
|
+
return parsed.ok === true || parsed.success === true || parsed.status === "ok";
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return exitCode === 0;
|
|
305
|
+
}
|
|
306
|
+
function recordResult(bucket, key, passed, stdout, stderr, exitCode, canonicalId, onProgress, durationMs) {
|
|
307
|
+
if (passed) {
|
|
308
|
+
bucket.passed.push(key);
|
|
309
|
+
onProgress?.(canonicalId, key, "pass", durationMs);
|
|
310
|
+
} else {
|
|
311
|
+
bucket.failed.push(key);
|
|
312
|
+
bucket.errors[key] = stderr || stdout || `Exit code ${exitCode}`;
|
|
313
|
+
onProgress?.(canonicalId, key, "fail", durationMs);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function runInRepoRoot(check, canonicalId, resolvedArgs, rootDir, bucket, onProgress) {
|
|
317
|
+
const startMs = Date.now();
|
|
318
|
+
const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, rootDir, check.timeoutMs ?? 12e4);
|
|
319
|
+
recordResult(bucket, rootDir, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
|
|
320
|
+
}
|
|
321
|
+
function runInScopePath(check, canonicalId, resolvedArgs, packages, rootDir, bucket, onProgress) {
|
|
322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const pkg of packages) {
|
|
324
|
+
if (seen.has(pkg.repo)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
seen.add(pkg.repo);
|
|
328
|
+
const scopeDir = resolve(rootDir, pkg.repo);
|
|
329
|
+
if (!existsSync(scopeDir)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const startMs = Date.now();
|
|
333
|
+
const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, scopeDir, check.timeoutMs ?? 12e4);
|
|
334
|
+
recordResult(bucket, pkg.repo, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function getPnpmScriptName(check) {
|
|
338
|
+
if (check.command !== "pnpm") {
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
const args = check.args ?? [];
|
|
342
|
+
if (args[0] === "run" && args[1]) {
|
|
343
|
+
return args[1];
|
|
344
|
+
}
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
function hasNpmScript(pkgDir, scriptName) {
|
|
348
|
+
try {
|
|
349
|
+
const pkgJson = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
|
|
350
|
+
return typeof pkgJson?.scripts?.[scriptName] === "string";
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function runPerPackage(check, canonicalId, resolvedArgs, packages, bucket, onProgress) {
|
|
356
|
+
const sortedPackages = check.ordered ? sortByBuildLayers(packages) : packages;
|
|
357
|
+
const scriptName = getPnpmScriptName(check);
|
|
358
|
+
for (const pkg of sortedPackages) {
|
|
359
|
+
if (scriptName && !hasNpmScript(pkg.dir, scriptName)) {
|
|
360
|
+
bucket.skipped.push(pkg.name);
|
|
361
|
+
onProgress?.(canonicalId, pkg.name, "skip");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const startMs = Date.now();
|
|
365
|
+
const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, pkg.dir, check.timeoutMs ?? 12e4);
|
|
366
|
+
recordResult(bucket, pkg.name, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function runCustomChecks(checks, packages, rootDir, onProgress) {
|
|
370
|
+
const results = {};
|
|
371
|
+
for (const check of checks) {
|
|
372
|
+
const canonicalId = ID_MAP[check.id.toLowerCase()] ?? check.id;
|
|
373
|
+
if (!results[canonicalId]) {
|
|
374
|
+
results[canonicalId] = emptyResult();
|
|
375
|
+
}
|
|
376
|
+
const bucket = results[canonicalId];
|
|
377
|
+
const args = check.args ?? [];
|
|
378
|
+
const resolvedArgs = args.map(
|
|
379
|
+
(arg) => arg.match(/\.(sh|js|ts|mjs|cjs)$/) && !arg.startsWith("/") ? join(rootDir, arg) : arg
|
|
380
|
+
);
|
|
381
|
+
const runIn = check.runIn ?? "perPackage";
|
|
382
|
+
if (runIn === "repoRoot") {
|
|
383
|
+
runInRepoRoot(check, canonicalId, resolvedArgs, rootDir, bucket, onProgress);
|
|
384
|
+
} else if (runIn === "scopePath") {
|
|
385
|
+
runInScopePath(check, canonicalId, resolvedArgs, packages, rootDir, bucket, onProgress);
|
|
386
|
+
} else {
|
|
387
|
+
runPerPackage(check, canonicalId, resolvedArgs, packages, bucket, onProgress);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return results;
|
|
391
|
+
}
|
|
392
|
+
function loadCache(rootDir) {
|
|
393
|
+
const cachePath = join(rootDir, PATHS.CACHE);
|
|
394
|
+
if (!existsSync(cachePath)) {
|
|
395
|
+
return {};
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
399
|
+
} catch {
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function saveCache(rootDir, cache) {
|
|
404
|
+
const cachePath = join(rootDir, PATHS.CACHE);
|
|
405
|
+
const dir = dirname(cachePath);
|
|
406
|
+
if (!existsSync(dir)) {
|
|
407
|
+
mkdirSync(dir, { recursive: true });
|
|
408
|
+
}
|
|
409
|
+
writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
410
|
+
}
|
|
411
|
+
function computePackageHash(pkgDir) {
|
|
412
|
+
const hash = createHash("sha256");
|
|
413
|
+
const pkgJsonPath = join(pkgDir, "package.json");
|
|
414
|
+
if (existsSync(pkgJsonPath)) {
|
|
415
|
+
hash.update(readFileSync(pkgJsonPath));
|
|
416
|
+
}
|
|
417
|
+
const srcDir = join(pkgDir, "src");
|
|
418
|
+
if (existsSync(srcDir)) {
|
|
419
|
+
hashDirectory(srcDir, hash);
|
|
420
|
+
}
|
|
421
|
+
return hash.digest("hex");
|
|
422
|
+
}
|
|
423
|
+
function hashDirectory(dir, hash) {
|
|
424
|
+
const entries = readdirSync(dir).sort();
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
const fullPath = join(dir, entry);
|
|
427
|
+
const stat = statSync(fullPath);
|
|
428
|
+
if (stat.isDirectory()) {
|
|
429
|
+
hashDirectory(fullPath, hash);
|
|
430
|
+
} else if (stat.isFile()) {
|
|
431
|
+
hash.update(fullPath);
|
|
432
|
+
hash.update(readFileSync(fullPath));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function hasPackageChanged(pkgDir, pkgName, cache) {
|
|
437
|
+
const currentHash = computePackageHash(pkgDir);
|
|
438
|
+
const cached = cache[pkgName];
|
|
439
|
+
if (!cached) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return cached.hash !== currentHash;
|
|
443
|
+
}
|
|
444
|
+
function updateCacheEntry(pkgDir, pkgName, cache) {
|
|
445
|
+
const hash = computePackageHash(pkgDir);
|
|
446
|
+
return {
|
|
447
|
+
...cache,
|
|
448
|
+
[pkgName]: { hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function needsRebuild(pkgDir) {
|
|
452
|
+
const srcDir = join(pkgDir, "src");
|
|
453
|
+
const distDir = join(pkgDir, "dist");
|
|
454
|
+
if (!existsSync(distDir)) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (!existsSync(srcDir)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const srcMtime = getLatestMtime(srcDir);
|
|
461
|
+
const distMtime = getLatestMtime(distDir);
|
|
462
|
+
return srcMtime > distMtime;
|
|
463
|
+
}
|
|
464
|
+
function getLatestMtime(dir) {
|
|
465
|
+
let latest = 0;
|
|
466
|
+
try {
|
|
467
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
468
|
+
for (const entry of entries) {
|
|
469
|
+
const fullPath = join(dir, entry.name);
|
|
470
|
+
if (entry.isDirectory()) {
|
|
471
|
+
latest = Math.max(latest, getLatestMtime(fullPath));
|
|
472
|
+
} else {
|
|
473
|
+
latest = Math.max(latest, statSync(fullPath).mtimeMs);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return latest;
|
|
479
|
+
}
|
|
480
|
+
function runBuildCheck(options) {
|
|
481
|
+
const { packages, noCache, onProgress } = options;
|
|
482
|
+
const result = { passed: [], failed: [], skipped: [], errors: {} };
|
|
483
|
+
const sorted = sortByBuildLayers(packages);
|
|
484
|
+
for (const pkg of sorted) {
|
|
485
|
+
if (!noCache && !needsRebuild(pkg.dir)) {
|
|
486
|
+
result.skipped.push(pkg.name);
|
|
487
|
+
onProgress?.(pkg.name, "skip");
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const startMs = Date.now();
|
|
491
|
+
try {
|
|
492
|
+
execSync("pnpm run build", {
|
|
493
|
+
cwd: pkg.dir,
|
|
494
|
+
encoding: "utf-8",
|
|
495
|
+
timeout: 12e4,
|
|
496
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
497
|
+
});
|
|
498
|
+
const durationMs = Date.now() - startMs;
|
|
499
|
+
result.passed.push(pkg.name);
|
|
500
|
+
onProgress?.(pkg.name, "pass", durationMs);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
const durationMs = Date.now() - startMs;
|
|
503
|
+
result.failed.push(pkg.name);
|
|
504
|
+
const rawErr = (err.stderr || err.stdout || err.message || "").trim();
|
|
505
|
+
result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Build failed (exit code ${err.status ?? 1})`;
|
|
506
|
+
onProgress?.(pkg.name, "fail", durationMs);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
function runLintCheck(options) {
|
|
512
|
+
const { packages, onProgress } = options;
|
|
513
|
+
const result = { passed: [], failed: [], skipped: [], errors: {} };
|
|
514
|
+
for (const pkg of packages) {
|
|
515
|
+
const srcDir = join(pkg.dir, "src");
|
|
516
|
+
if (!existsSync(srcDir)) {
|
|
517
|
+
result.skipped.push(pkg.name);
|
|
518
|
+
onProgress?.(pkg.name, "skip");
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const startMs = Date.now();
|
|
522
|
+
try {
|
|
523
|
+
execSync("pnpm exec eslint .", {
|
|
524
|
+
cwd: pkg.dir,
|
|
525
|
+
encoding: "utf-8",
|
|
526
|
+
timeout: 6e4,
|
|
527
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
528
|
+
});
|
|
529
|
+
result.passed.push(pkg.name);
|
|
530
|
+
onProgress?.(pkg.name, "pass", Date.now() - startMs);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
result.failed.push(pkg.name);
|
|
533
|
+
const rawErr = (err.stdout || err.stderr || err.message || "").trim();
|
|
534
|
+
result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Lint failed (exit code ${err.status ?? 1})`;
|
|
535
|
+
onProgress?.(pkg.name, "fail", Date.now() - startMs);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
function runTypeCheck(options) {
|
|
541
|
+
const { packages, onProgress } = options;
|
|
542
|
+
const result = { passed: [], failed: [], skipped: [], errors: {} };
|
|
543
|
+
for (const pkg of packages) {
|
|
544
|
+
const tsconfigPath = join(pkg.dir, "tsconfig.json");
|
|
545
|
+
if (!existsSync(tsconfigPath)) {
|
|
546
|
+
result.skipped.push(pkg.name);
|
|
547
|
+
onProgress?.(pkg.name, "skip");
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const startMs = Date.now();
|
|
551
|
+
try {
|
|
552
|
+
execSync("pnpm exec tsc --noEmit", {
|
|
553
|
+
cwd: pkg.dir,
|
|
554
|
+
encoding: "utf-8",
|
|
555
|
+
timeout: 12e4,
|
|
556
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
557
|
+
});
|
|
558
|
+
result.passed.push(pkg.name);
|
|
559
|
+
onProgress?.(pkg.name, "pass", Date.now() - startMs);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
result.failed.push(pkg.name);
|
|
562
|
+
const rawErr = (err.stdout || err.stderr || err.message || "").trim();
|
|
563
|
+
result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Type check failed (exit code ${err.status ?? 1})`;
|
|
564
|
+
onProgress?.(pkg.name, "fail", Date.now() - startMs);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
function runTestCheck(options) {
|
|
570
|
+
const { packages, onProgress } = options;
|
|
571
|
+
const result = { passed: [], failed: [], skipped: [], errors: {} };
|
|
572
|
+
for (const pkg of packages) {
|
|
573
|
+
let pkgJson;
|
|
574
|
+
try {
|
|
575
|
+
pkgJson = JSON.parse(
|
|
576
|
+
readFileSync(join(pkg.dir, "package.json"), "utf-8")
|
|
577
|
+
);
|
|
578
|
+
} catch {
|
|
579
|
+
result.skipped.push(pkg.name);
|
|
580
|
+
onProgress?.(pkg.name, "skip");
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (!pkgJson.scripts?.test) {
|
|
584
|
+
result.skipped.push(pkg.name);
|
|
585
|
+
onProgress?.(pkg.name, "skip");
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const startMs = Date.now();
|
|
589
|
+
try {
|
|
590
|
+
execSync("pnpm run test", {
|
|
591
|
+
cwd: pkg.dir,
|
|
592
|
+
encoding: "utf-8",
|
|
593
|
+
timeout: 12e4,
|
|
594
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
595
|
+
});
|
|
596
|
+
result.passed.push(pkg.name);
|
|
597
|
+
onProgress?.(pkg.name, "pass", Date.now() - startMs);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
result.failed.push(pkg.name);
|
|
600
|
+
const rawErr = (err.stdout || err.stderr || err.message || "").trim();
|
|
601
|
+
result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Test failed (exit code ${err.status ?? 1})`;
|
|
602
|
+
onProgress?.(pkg.name, "fail", Date.now() - startMs);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
function saveLastRun(rootDir, results, packages, submodules) {
|
|
608
|
+
const filePath = join(rootDir, PATHS.LAST_RUN);
|
|
609
|
+
const dir = dirname(filePath);
|
|
610
|
+
if (!existsSync(dir)) {
|
|
611
|
+
mkdirSync(dir, { recursive: true });
|
|
612
|
+
}
|
|
613
|
+
const data = {
|
|
614
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
615
|
+
results,
|
|
616
|
+
packages: packages.map((p) => ({
|
|
617
|
+
name: p.name,
|
|
618
|
+
dir: p.dir,
|
|
619
|
+
relativePath: p.relativePath,
|
|
620
|
+
repo: p.repo,
|
|
621
|
+
submodule: p.submodule
|
|
622
|
+
})),
|
|
623
|
+
submodules
|
|
624
|
+
};
|
|
625
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
626
|
+
}
|
|
627
|
+
function loadLastRun(rootDir) {
|
|
628
|
+
const filePath = join(rootDir, PATHS.LAST_RUN);
|
|
629
|
+
if (!existsSync(filePath)) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/runner/qa-orchestrator.ts
|
|
640
|
+
var SKIP_ALIASES = {
|
|
641
|
+
types: "typecheck",
|
|
642
|
+
"type-check": "typecheck",
|
|
643
|
+
tests: "test"
|
|
644
|
+
};
|
|
645
|
+
function runBuiltinChecks(options, packages, skipSet, results) {
|
|
646
|
+
const { rootDir, noCache } = options;
|
|
647
|
+
if (!skipSet.has("build")) {
|
|
648
|
+
results.build = runBuildCheck({
|
|
649
|
+
packages,
|
|
650
|
+
noCache,
|
|
651
|
+
onProgress: (pkg, status, durationMs) => options.onProgress?.("build", pkg, status, durationMs)
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (!skipSet.has("lint")) {
|
|
655
|
+
results.lint = runLintCheck({
|
|
656
|
+
packages,
|
|
657
|
+
onProgress: (pkg, status, durationMs) => options.onProgress?.("lint", pkg, status, durationMs)
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (!skipSet.has("typecheck")) {
|
|
661
|
+
results.typeCheck = runTypeCheck({
|
|
662
|
+
packages,
|
|
663
|
+
onProgress: (pkg, status, durationMs) => options.onProgress?.("typeCheck", pkg, status, durationMs)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (!skipSet.has("test")) {
|
|
667
|
+
results.test = runTestCheck({
|
|
668
|
+
packages,
|
|
669
|
+
onProgress: (pkg, status, durationMs) => options.onProgress?.("test", pkg, status, durationMs)
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async function runQA(options) {
|
|
674
|
+
const { rootDir, noCache } = options;
|
|
675
|
+
const skipSet = new Set(
|
|
676
|
+
(options.skipChecks ?? []).map((s) => SKIP_ALIASES[s.toLowerCase()] ?? s.toLowerCase())
|
|
677
|
+
);
|
|
678
|
+
const filter = { package: options.package, repo: options.repo, scope: options.scope };
|
|
679
|
+
const packages = getWorkspacePackages(rootDir, filter, options.packagesConfig);
|
|
680
|
+
let cache = noCache ? {} : loadCache(rootDir);
|
|
681
|
+
const results = {};
|
|
682
|
+
if (options.checks && options.checks.length > 0) {
|
|
683
|
+
const activeChecks = skipSet.size > 0 ? options.checks.filter((c) => !skipSet.has(c.id.toLowerCase())) : options.checks;
|
|
684
|
+
Object.assign(results, runCustomChecks(
|
|
685
|
+
activeChecks,
|
|
686
|
+
packages,
|
|
687
|
+
rootDir,
|
|
688
|
+
(checkId, pkg, status, durationMs) => {
|
|
689
|
+
options.onProgress?.(checkId, pkg, status, durationMs);
|
|
690
|
+
}
|
|
691
|
+
));
|
|
692
|
+
} else {
|
|
693
|
+
runBuiltinChecks(options, packages, skipSet, results);
|
|
694
|
+
}
|
|
695
|
+
if (!noCache) {
|
|
696
|
+
for (const pkg of packages) {
|
|
697
|
+
cache = updateCacheEntry(pkg.dir, pkg.name, cache);
|
|
698
|
+
}
|
|
699
|
+
saveCache(rootDir, cache);
|
|
700
|
+
}
|
|
701
|
+
const submodules = {};
|
|
702
|
+
for (const pkg of packages) {
|
|
703
|
+
if (pkg.submodule && !submodules[pkg.repo]) {
|
|
704
|
+
submodules[pkg.repo] = pkg.submodule;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
saveLastRun(rootDir, results, packages, Object.keys(submodules).length > 0 ? submodules : void 0);
|
|
708
|
+
return { results, packages };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export { collectSubmoduleInfo, computePackageHash, getSubmoduleInfo, getWorkspacePackages, hasPackageChanged, loadCache, loadLastRun, runBuildCheck, runLintCheck, runQA, runTestCheck, runTypeCheck, saveCache, saveLastRun, updateCacheEntry };
|
|
712
|
+
//# sourceMappingURL=index.js.map
|
|
713
|
+
//# sourceMappingURL=index.js.map
|