ci: add A2UI bundling script and CI updates for tests
This commit is contained in:
parent
176768770e
commit
e6f529f72d
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -156,6 +156,10 @@ jobs:
|
|||||||
pnpm -v
|
pnpm -v
|
||||||
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
|
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
|
||||||
|
|
||||||
|
- name: Prebuild A2UI bundle (bun tests)
|
||||||
|
if: ${{ matrix.runtime == 'bun' && matrix.task == 'test' }}
|
||||||
|
run: pnpm canvas:a2ui:bundle
|
||||||
|
|
||||||
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
||||||
run: ${{ matrix.command }}
|
run: ${{ matrix.command }}
|
||||||
|
|
||||||
|
|||||||
112
scripts/bundle-a2ui.ts
Normal file
112
scripts/bundle-a2ui.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
const HASH_FILE = path.join(repoRoot, "src", "canvas-host", "a2ui", ".bundle.hash");
|
||||||
|
const OUTPUT_FILE = path.join(repoRoot, "src", "canvas-host", "a2ui", "a2ui.bundle.js");
|
||||||
|
|
||||||
|
const INPUT_PATHS = [
|
||||||
|
path.join(repoRoot, "package.json"),
|
||||||
|
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||||
|
path.join(repoRoot, "vendor", "a2ui", "renderers", "lit"),
|
||||||
|
path.join(repoRoot, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI"),
|
||||||
|
];
|
||||||
|
|
||||||
|
async function exists(p: string) {
|
||||||
|
try {
|
||||||
|
await fs.stat(p);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listFiles(root: string): Promise<string[]> {
|
||||||
|
const out: string[] = [];
|
||||||
|
async function walk(dir: string) {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const ent of entries) {
|
||||||
|
const full = path.join(dir, ent.name);
|
||||||
|
if (ent.isDirectory()) {
|
||||||
|
await walk(full);
|
||||||
|
} else if (ent.isFile()) {
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(root);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectInputFiles(): Promise<string[]> {
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const p of INPUT_PATHS) {
|
||||||
|
const st = await fs.stat(p);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
files.push(...(await listFiles(p)));
|
||||||
|
} else {
|
||||||
|
files.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort((a, b) => a.localeCompare(b, "en"));
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256File(filePath: string): Promise<string> {
|
||||||
|
const buf = await fs.readFile(filePath);
|
||||||
|
return crypto.createHash("sha256").update(buf).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeHash(): Promise<string> {
|
||||||
|
const inputs = await collectInputFiles();
|
||||||
|
const h = crypto.createHash("sha256");
|
||||||
|
for (const p of inputs) {
|
||||||
|
// include relative path to avoid collisions and keep stable ordering
|
||||||
|
const rel = path.relative(repoRoot, p).replace(/\\/g, "/");
|
||||||
|
h.update(rel);
|
||||||
|
h.update("\0");
|
||||||
|
h.update(await fs.readFile(p));
|
||||||
|
h.update("\0");
|
||||||
|
}
|
||||||
|
return h.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(cmd: string, args: string[], label: string) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const child = spawn(cmd, args, { stdio: "inherit", cwd: repoRoot, shell: process.platform === "win32" });
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
if (code === 0) return resolve();
|
||||||
|
reject(new Error(`${label} failed (code=${code ?? "?"}${signal ? ` signal=${signal}` : ""})`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const currentHash = await computeHash();
|
||||||
|
const previousHash = (await exists(HASH_FILE)) ? (await fs.readFile(HASH_FILE, "utf8")).trim() : null;
|
||||||
|
|
||||||
|
if (previousHash && previousHash === currentHash && (await exists(OUTPUT_FILE))) {
|
||||||
|
console.log("A2UI bundle up to date; skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build vendor A2UI lit renderer TS output, then bundle our bootstrap.
|
||||||
|
await run("pnpm", ["-s", "exec", "tsc", "-p", "vendor/a2ui/renderers/lit/tsconfig.json"], "A2UI lit tsc");
|
||||||
|
await run("pnpm", ["-s", "exec", "rolldown", "-c", "apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs"], "A2UI rolldown");
|
||||||
|
|
||||||
|
await fs.writeFile(HASH_FILE, `${currentHash}\n`, "utf8");
|
||||||
|
|
||||||
|
// sanity
|
||||||
|
const outHash = await sha256File(OUTPUT_FILE);
|
||||||
|
if (!outHash) throw new Error("A2UI bundle not generated");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
|
||||||
|
console.error(String(err));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -3,6 +3,16 @@ import os from "node:os";
|
|||||||
|
|
||||||
const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||||
|
|
||||||
|
function runOnce(args, label) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(pnpm, args, { stdio: "inherit", shell: process.platform === "win32" });
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
if (code === 0) return resolve();
|
||||||
|
reject(new Error(`${label} failed (code=${code ?? "?"}${signal ? ` signal=${signal}` : ""})`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const runs = [
|
const runs = [
|
||||||
{
|
{
|
||||||
name: "unit",
|
name: "unit",
|
||||||
@ -44,7 +54,7 @@ const WARNING_SUPPRESSION_FLAGS = [
|
|||||||
"--disable-warning=DEP0060",
|
"--disable-warning=DEP0060",
|
||||||
];
|
];
|
||||||
|
|
||||||
const runOnce = (entry, extraArgs = []) =>
|
const runTestOnce = (entry, extraArgs = []) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const args = maxWorkers
|
const args = maxWorkers
|
||||||
? [...entry.args, "--maxWorkers", String(maxWorkers), ...windowsCiArgs, ...extraArgs]
|
? [...entry.args, "--maxWorkers", String(maxWorkers), ...windowsCiArgs, ...extraArgs]
|
||||||
@ -67,10 +77,10 @@ const runOnce = (entry, extraArgs = []) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const run = async (entry) => {
|
const run = async (entry) => {
|
||||||
if (shardCount <= 1) return runOnce(entry);
|
if (shardCount <= 1) return runTestOnce(entry);
|
||||||
for (let shardIndex = 1; shardIndex <= shardCount; shardIndex += 1) {
|
for (let shardIndex = 1; shardIndex <= shardCount; shardIndex += 1) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const code = await runOnce(entry, ["--shard", `${shardIndex}/${shardCount}`]);
|
const code = await runTestOnce(entry, ["--shard", `${shardIndex}/${shardCount}`]);
|
||||||
if (code !== 0) return code;
|
if (code !== 0) return code;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@ -85,6 +95,10 @@ const shutdown = (signal) => {
|
|||||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
|
||||||
|
// Some unit tests expect the gateway-hosted A2UI scaffold + bundle to be present.
|
||||||
|
// The bundle is a generated artifact (gitignored), so ensure it's built before running Vitest.
|
||||||
|
await runOnce(["canvas:a2ui:bundle"], "A2UI bundle");
|
||||||
|
|
||||||
const parallelCodes = await Promise.all(parallelRuns.map(run));
|
const parallelCodes = await Promise.all(parallelRuns.map(run));
|
||||||
const failedParallel = parallelCodes.find((code) => code !== 0);
|
const failedParallel = parallelCodes.find((code) => code !== 0);
|
||||||
if (failedParallel !== undefined) {
|
if (failedParallel !== undefined) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user