mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
ci: fail build-artifacts when committed plugin bundles go stale (#112489)
This commit is contained in:
committed by
GitHub
parent
b8ccb5dfa9
commit
01be7aa609
@@ -1108,6 +1108,12 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
|
||||
# Reruns the (warm, seconds-cheap) asset hooks and fails on drift so
|
||||
# packages/-only PRs cannot land stale committed plugin bundles; the
|
||||
# extension byte-equality suites do not run for those diffs.
|
||||
- name: Check bundled plugin generated assets
|
||||
run: pnpm plugins:assets:check
|
||||
|
||||
- name: Pack built runtime artifacts
|
||||
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime packages/*/dist
|
||||
|
||||
|
||||
@@ -1601,6 +1601,7 @@
|
||||
"plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json",
|
||||
"plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary",
|
||||
"plugins:assets:build": "node scripts/bundled-plugin-assets.mjs --phase build",
|
||||
"plugins:assets:check": "node scripts/bundled-plugin-assets.mjs --phase build --check",
|
||||
"plugins:assets:copy": "node scripts/bundled-plugin-assets.mjs --phase copy",
|
||||
"plugins:inventory:check": "node scripts/generate-plugin-inventory-doc.mjs --check",
|
||||
"plugins:inventory:gen": "node scripts/generate-plugin-inventory-doc.mjs --write",
|
||||
|
||||
@@ -17,9 +17,14 @@ export function readBundledPluginAssetHooks(options?: Record<string, unknown>):
|
||||
*/
|
||||
export function runBundledPluginAssetHooks(options?: Record<string, unknown>): Promise<void>;
|
||||
/**
|
||||
* Parses `--phase` and repeated `--plugin` flags for asset hook scripts.
|
||||
* Lists declared generated source-tree outputs that differ from the committed bytes.
|
||||
*/
|
||||
export function listStaleGeneratedPluginAssets(options?: Record<string, unknown>): string[];
|
||||
/**
|
||||
* Parses `--phase`, repeated `--plugin`, and `--check` flags for asset hook scripts.
|
||||
*/
|
||||
export function parseBundledPluginAssetArgs(argv: unknown): {
|
||||
check: boolean;
|
||||
phase: unknown;
|
||||
plugins: unknown[];
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mjs";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const VALID_PHASES = new Set(["build", "copy"]);
|
||||
@@ -139,12 +140,42 @@ export async function runBundledPluginAssetHooks(options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `--phase` and repeated `--plugin` flags for asset hook scripts.
|
||||
* Lists declared generated source-tree outputs that differ from the committed
|
||||
* bytes. Committed buildOutputs must match a fresh hook run; PR node-test
|
||||
* selection skips extension suites for packages-only diffs, so this check is
|
||||
* the guard that keeps upstream changes from landing stale committed bundles.
|
||||
*/
|
||||
export function listStaleGeneratedPluginAssets(options = {}) {
|
||||
const repoRoot = options.rootDir ?? rootDir;
|
||||
const sources = listGeneratedExtensionAssetSources({ rootDir: repoRoot });
|
||||
if (sources.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const result = spawnSync("git", ["status", "--porcelain", "--", ...sources], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`git status failed for generated plugin assets: ${result.stderr?.trim() || result.status}`,
|
||||
);
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.slice(3).trim())
|
||||
.filter(Boolean)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `--phase`, repeated `--plugin`, and `--check` flags for asset hook scripts.
|
||||
*/
|
||||
export function parseBundledPluginAssetArgs(argv) {
|
||||
const args = [...argv];
|
||||
const plugins = [];
|
||||
let phase = null;
|
||||
let check = false;
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
@@ -167,19 +198,44 @@ export function parseBundledPluginAssetArgs(argv) {
|
||||
plugins.push(arg.slice("--plugin=".length));
|
||||
continue;
|
||||
}
|
||||
if (arg === "--check") {
|
||||
check = true;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown bundled plugin asset argument: ${String(arg)}`);
|
||||
}
|
||||
|
||||
if (!VALID_PHASES.has(phase)) {
|
||||
throw new Error(`Expected --phase ${[...VALID_PHASES].join("|")}`);
|
||||
}
|
||||
// The stale-asset scan covers every declared buildOutput, so a filtered run
|
||||
// would fail on drift it never rebuilt; keep check runs whole-repo.
|
||||
if (check && phase !== "build") {
|
||||
throw new Error("--check requires --phase build");
|
||||
}
|
||||
if (check && plugins.length > 0) {
|
||||
throw new Error("--check cannot be combined with --plugin filters");
|
||||
}
|
||||
|
||||
return { phase, plugins };
|
||||
return { check, phase, plugins };
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
try {
|
||||
await runBundledPluginAssetHooks(parseBundledPluginAssetArgs(process.argv.slice(2)));
|
||||
const args = parseBundledPluginAssetArgs(process.argv.slice(2));
|
||||
await runBundledPluginAssetHooks(args);
|
||||
if (args.check) {
|
||||
const stale = listStaleGeneratedPluginAssets();
|
||||
if (stale.length > 0) {
|
||||
console.error("Generated bundled plugin assets differ from the committed bytes:");
|
||||
for (const source of stale) {
|
||||
console.error(` - ${source}`);
|
||||
}
|
||||
console.error("Rebuild with `pnpm plugins:assets:build` and commit the regenerated files.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Generated bundled plugin assets match the committed bytes.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Bundled Plugin Assets tests cover bundled plugin assets script behavior.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildDiscordActivitySdk } from "../../scripts/build-discord-activity-sdk.mjs";
|
||||
import {
|
||||
listStaleGeneratedPluginAssets,
|
||||
parseBundledPluginAssetArgs,
|
||||
readBundledPluginAssetHooks,
|
||||
} from "../../scripts/bundled-plugin-assets.mjs";
|
||||
@@ -169,8 +171,49 @@ describe("bundled plugin assets", () => {
|
||||
|
||||
it("parses phase and plugin filters", () => {
|
||||
expect(parseBundledPluginAssetArgs(["--phase", "build", "--plugin=canvas"])).toEqual({
|
||||
check: false,
|
||||
phase: "build",
|
||||
plugins: ["canvas"],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses whole-repo check runs and rejects filtered or copy-phase checks", () => {
|
||||
expect(parseBundledPluginAssetArgs(["--phase", "build", "--check"])).toEqual({
|
||||
check: true,
|
||||
phase: "build",
|
||||
plugins: [],
|
||||
});
|
||||
expect(() => parseBundledPluginAssetArgs(["--phase", "copy", "--check"])).toThrow(
|
||||
"--check requires --phase build",
|
||||
);
|
||||
expect(() =>
|
||||
parseBundledPluginAssetArgs(["--phase", "build", "--check", "--plugin=canvas"]),
|
||||
).toThrow("--check cannot be combined with --plugin filters");
|
||||
});
|
||||
|
||||
it("reports declared generated outputs that differ from the committed bytes", async () => {
|
||||
await withPluginAssetFixture(async (rootDir) => {
|
||||
const generatedPath = path.join(
|
||||
rootDir,
|
||||
"extensions",
|
||||
"canvas",
|
||||
"assets",
|
||||
"generated-runtime.js",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(generatedPath), { recursive: true });
|
||||
fs.writeFileSync(generatedPath, "export const generated = 1;\n");
|
||||
const git = (...args: string[]) =>
|
||||
execFileSync("git", args, { cwd: rootDir, stdio: ["ignore", "pipe", "pipe"] });
|
||||
git("init", "--quiet");
|
||||
git("-c", "user.email=t@t", "-c", "user.name=t", "add", ".");
|
||||
git("-c", "user.email=t@t", "-c", "user.name=t", "commit", "--quiet", "-m", "init");
|
||||
|
||||
expect(listStaleGeneratedPluginAssets({ rootDir })).toEqual([]);
|
||||
|
||||
fs.writeFileSync(generatedPath, "export const generated = 2;\n");
|
||||
expect(listStaleGeneratedPluginAssets({ rootDir })).toEqual([
|
||||
"extensions/canvas/assets/generated-runtime.js",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user