mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-03 12:27:55 +08:00
* fix(tools-pack): use junctions for Windows standalone peer deps * fix(desktop): expose IPC during startup * fix(tools-pack): preserve Windows inspect diagnostics * fix(tools-pack): report Windows inspect status errors * fix(packaged): use Electron net fetch for app protocol * fix(packaged): load Windows renderer from web sidecar * fix(desktop): show Windows packaged window during startup * fix(packaged): disable Windows GPU startup * fix(tools-pack): keep Windows core smoke observable * fix(packaged): remove Windows startup probes * fix(tools-pack): trace Windows desktop IPC status * fix(tools-pack): add Windows IPC diagnose loop * fix(release): default beta-s Windows updater feed * chore: clean merged test eof * refactor(release): unify prerelease channel model * chore(release): close prerelease doc escape hatches * refactor(release): converge release channel workflows * fix(release): install toolchain in metadata jobs * fix(release): build release package before contracts * chore(release): bump development version to 0.10.1 * fix(e2e): seed windows packaged smoke runtime config * fix(release): install toolchain for metadata publish * fix(release): materialize betas metadata checkout * chore(release): bump development version to 0.10.2 * fix(release): allow betas metadata cold start from s3 * fix(e2e): support betas packaged update scenarios * fix(release): pass betas channel into packaged smoke * fix(release): set betas channel during self-hosted builds * fix(release): verify counted channel reservations * fix(release): use pnpm cmd for betas windows publish * fix(release): add betas manifest artifact fallback * fix(release): skip beta-s public metadata fetch * fix(release): read beta-s manifests from storage * fix(release): cache beta windows tools-pack builds * fix(release): inline beta mac tools-pack builds * fix(pack): deep sign unsigned mac bundles * docs(pack): document payload-first beta updater validation * fix(release): align preview tools-pack cache flow * fix(release): align prerelease tools-pack cache flow * fix(release): pass github token to prerelease metadata * fix(release): setup pnpm before feishu notify * fix(release): add stable dry-run prepublish flow * fix(release): accept completed prerelease metadata gate * fix(release): require stable release branches * fix(release): converge r2 access checks * fix(updater): use release channel parser for defaults * fix(updater): harden windows payload relaunch * fix(release): converge updater smoke fixture contract * test(e2e): require silent updater fixture output * fix(release): align stable windows smoke build path * fix(ci): include release workspace in validation * fix(ci): repair release validation lanes Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex) * fix(ci): restore zero-install Feishu notification Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com>
444 lines
13 KiB
TypeScript
444 lines
13 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
|
|
import { uiP0CiMatrix, visualCiMatrix } from "../e2e/lib/playwright/suites.ts";
|
|
|
|
type CiMode = "hot" | "full";
|
|
|
|
type ScopeOutputs = {
|
|
daemon_tests_required: boolean;
|
|
web_tests_required: boolean;
|
|
tools_dev_tests_required: boolean;
|
|
tools_pack_tests_required: boolean;
|
|
nix_validation_required: boolean;
|
|
ui_p0_validation_required: boolean;
|
|
visual_validation_required: boolean;
|
|
docker_validation_required: boolean;
|
|
workspace_validation_required: boolean;
|
|
};
|
|
|
|
type ScopePlan = ScopeOutputs & {
|
|
ci_mode: CiMode;
|
|
run_docker_build: boolean;
|
|
run_e2e_vitest: boolean;
|
|
run_nix_validation: boolean;
|
|
run_playwright_critical: boolean;
|
|
run_playwright_visual: boolean;
|
|
run_preflight: boolean;
|
|
run_ui_p0: boolean;
|
|
run_web_workspace_tests: boolean;
|
|
run_windows_tools_pack_payload_tests: boolean;
|
|
run_workspace_unit_tests: boolean;
|
|
ui_p0_matrix: string;
|
|
visual_matrix: string;
|
|
};
|
|
|
|
type GitHubEvent = {
|
|
pull_request?: {
|
|
number?: number;
|
|
};
|
|
inputs?: {
|
|
ci_mode?: string;
|
|
};
|
|
};
|
|
|
|
const commandName = process.argv[2] ?? "github-output";
|
|
|
|
if (commandName === "github-output") {
|
|
writeGithubOutputs(createScopePlan());
|
|
} else if (commandName === "print") {
|
|
console.log(JSON.stringify(createScopePlan(), null, 2));
|
|
} else if (commandName === "help") {
|
|
printUsage();
|
|
} else {
|
|
console.error(`Unknown scopes command: ${commandName}`);
|
|
printUsage();
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
function createScopePlan(): ScopePlan {
|
|
const outputs: ScopeOutputs = {
|
|
daemon_tests_required: false,
|
|
web_tests_required: false,
|
|
tools_dev_tests_required: false,
|
|
tools_pack_tests_required: false,
|
|
nix_validation_required: false,
|
|
ui_p0_validation_required: false,
|
|
visual_validation_required: false,
|
|
docker_validation_required: false,
|
|
workspace_validation_required: false,
|
|
};
|
|
|
|
const eventName = requiredEnv("GITHUB_EVENT_NAME");
|
|
const ciMode = resolveCiMode(eventName);
|
|
|
|
if (eventName === "pull_request") {
|
|
for (const file of changedPullRequestFiles()) {
|
|
applyChangedFile(file, outputs);
|
|
if (allScopeOutputsTrue(outputs)) break;
|
|
}
|
|
|
|
if (
|
|
outputs.daemon_tests_required ||
|
|
outputs.web_tests_required ||
|
|
outputs.tools_dev_tests_required ||
|
|
outputs.tools_pack_tests_required
|
|
) {
|
|
outputs.workspace_validation_required = true;
|
|
}
|
|
} else if (eventName === "workflow_dispatch" && ciMode === "hot") {
|
|
for (const file of changedManualFiles()) {
|
|
applyChangedFile(file, outputs);
|
|
if (allScopeOutputsTrue(outputs)) break;
|
|
}
|
|
} else {
|
|
outputs.daemon_tests_required = true;
|
|
outputs.web_tests_required = true;
|
|
outputs.tools_dev_tests_required = true;
|
|
outputs.tools_pack_tests_required = true;
|
|
outputs.nix_validation_required = true;
|
|
outputs.ui_p0_validation_required = true;
|
|
outputs.visual_validation_required = true;
|
|
outputs.docker_validation_required = true;
|
|
outputs.workspace_validation_required = true;
|
|
}
|
|
|
|
return {
|
|
...outputs,
|
|
...createRunPlan(outputs, ciMode),
|
|
ui_p0_matrix: JSON.stringify(uiP0CiMatrix),
|
|
visual_matrix: JSON.stringify(visualCiMatrix),
|
|
};
|
|
}
|
|
|
|
function resolveCiMode(eventName: string): CiMode {
|
|
if (eventName === "pull_request") return "hot";
|
|
if (eventName === "workflow_dispatch") {
|
|
const event = JSON.parse(readFileSync(requiredEnv("GITHUB_EVENT_PATH"), "utf8")) as GitHubEvent;
|
|
const input = event.inputs?.ci_mode ?? "full";
|
|
if (input === "hot" || input === "full") return input;
|
|
throw new Error(`Unsupported workflow_dispatch ci_mode: ${input}`);
|
|
}
|
|
return "full";
|
|
}
|
|
|
|
function createRunPlan(
|
|
outputs: ScopeOutputs,
|
|
ciMode: CiMode,
|
|
): Omit<ScopePlan, keyof ScopeOutputs | "ui_p0_matrix" | "visual_matrix"> {
|
|
const isFull = ciMode === "full";
|
|
|
|
return {
|
|
ci_mode: ciMode,
|
|
run_docker_build: isFull || outputs.docker_validation_required,
|
|
run_e2e_vitest: isFull || outputs.web_tests_required || outputs.ui_p0_validation_required,
|
|
run_nix_validation: isFull || outputs.nix_validation_required,
|
|
run_playwright_critical: isFull || (outputs.workspace_validation_required && !outputs.ui_p0_validation_required),
|
|
run_playwright_visual: isFull || outputs.visual_validation_required,
|
|
run_preflight: true,
|
|
run_ui_p0: isFull || outputs.ui_p0_validation_required,
|
|
run_web_workspace_tests: isFull || outputs.web_tests_required,
|
|
run_windows_tools_pack_payload_tests: isFull || outputs.tools_pack_tests_required,
|
|
run_workspace_unit_tests: true,
|
|
};
|
|
}
|
|
|
|
function changedPullRequestFiles(): string[] {
|
|
const eventPath = requiredEnv("GITHUB_EVENT_PATH");
|
|
const repository = requiredEnv("GITHUB_REPOSITORY");
|
|
const event = JSON.parse(readFileSync(eventPath, "utf8")) as GitHubEvent;
|
|
const prNumber = event.pull_request?.number;
|
|
if (prNumber == null) {
|
|
throw new Error("pull_request event payload did not include pull_request.number");
|
|
}
|
|
|
|
const stdout = runGh(["api", "--paginate", `repos/${repository}/pulls/${prNumber}/files`, "--jq", ".[].filename"]);
|
|
return stdout.split(/\r?\n/).filter(Boolean);
|
|
}
|
|
|
|
function changedManualFiles(): string[] {
|
|
const repository = requiredEnv("GITHUB_REPOSITORY");
|
|
const sha = requiredEnv("GITHUB_SHA");
|
|
const stdout = runGh([
|
|
"api",
|
|
"--paginate",
|
|
`repos/${repository}/compare/main...${sha}`,
|
|
"--jq",
|
|
'(.files // [])[] | select(.status != "removed") | .filename',
|
|
]);
|
|
return stdout.split(/\r?\n/).filter(Boolean);
|
|
}
|
|
|
|
function runGh(args: string[]): string {
|
|
const nodeScript = process.env.OPEN_DESIGN_GH_NODE_SCRIPT;
|
|
if (nodeScript != null && nodeScript.length > 0) {
|
|
return execFileSync(process.execPath, [nodeScript, ...args], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "inherit"],
|
|
});
|
|
}
|
|
return execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
}
|
|
|
|
function applyChangedFile(file: string, target: ScopeOutputs): void {
|
|
if (
|
|
startsWithAny(file, [
|
|
"apps/daemon/",
|
|
"packages/release/",
|
|
"packages/contracts/",
|
|
"packages/platform/",
|
|
"packages/sidecar/",
|
|
"packages/sidecar-proto/",
|
|
])
|
|
) {
|
|
target.daemon_tests_required = true;
|
|
}
|
|
|
|
if (
|
|
startsWithAny(file, [
|
|
"apps/web/",
|
|
"packages/release/",
|
|
"packages/components/",
|
|
"packages/contracts/",
|
|
"packages/host/",
|
|
"packages/platform/",
|
|
"packages/sidecar/",
|
|
"packages/sidecar-proto/",
|
|
])
|
|
) {
|
|
target.web_tests_required = true;
|
|
}
|
|
|
|
if (startsWithAny(file, ["scripts/", "assets/", "skills/", "prompt-templates/", "design-systems/", "design-templates/", "craft/"])) {
|
|
target.daemon_tests_required = true;
|
|
target.web_tests_required = true;
|
|
}
|
|
|
|
if (startsWithAny(file, ["tools/dev/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/"])) {
|
|
target.tools_dev_tests_required = true;
|
|
}
|
|
|
|
if (
|
|
startsWithAny(file, [
|
|
"tools/pack/",
|
|
"apps/packaged/",
|
|
"apps/desktop/",
|
|
"packages/release/",
|
|
"packages/components/",
|
|
"packages/host/",
|
|
"packages/platform/",
|
|
"packages/sidecar/",
|
|
"packages/sidecar-proto/",
|
|
])
|
|
) {
|
|
target.tools_pack_tests_required = true;
|
|
}
|
|
|
|
if (isWorkspaceManifestOrCiFile(file)) {
|
|
target.daemon_tests_required = true;
|
|
target.web_tests_required = true;
|
|
target.tools_dev_tests_required = true;
|
|
target.tools_pack_tests_required = true;
|
|
}
|
|
|
|
if (isUiP0RelevantFile(file)) {
|
|
target.ui_p0_validation_required = true;
|
|
}
|
|
|
|
if (isVisualRelevantFile(file)) {
|
|
target.visual_validation_required = true;
|
|
}
|
|
|
|
if (isDockerRelevantFile(file)) {
|
|
target.docker_validation_required = true;
|
|
}
|
|
|
|
if (isNixRelevantFile(file)) {
|
|
target.nix_validation_required = true;
|
|
}
|
|
|
|
if (!isWorkspaceValidationExemptFile(file)) {
|
|
target.workspace_validation_required = true;
|
|
}
|
|
}
|
|
|
|
function isWorkspaceManifestOrCiFile(file: string): boolean {
|
|
return (
|
|
file === "package.json" ||
|
|
file === "pnpm-lock.yaml" ||
|
|
file === "pnpm-workspace.yaml" ||
|
|
file === ".github/workflows/ci.yml" ||
|
|
/^apps\/[^/]+\/package\.json$/.test(file) ||
|
|
/^packages\/[^/]+\/package\.json$/.test(file) ||
|
|
/^tools\/[^/]+\/package\.json$/.test(file) ||
|
|
file === "e2e/package.json"
|
|
);
|
|
}
|
|
|
|
function isUiP0RelevantFile(file: string): boolean {
|
|
return (
|
|
startsWithAny(file, [
|
|
"apps/web/",
|
|
"apps/daemon/",
|
|
"packages/release/",
|
|
"packages/components/",
|
|
"packages/contracts/",
|
|
"packages/host/",
|
|
"packages/platform/",
|
|
"packages/sidecar/",
|
|
"packages/sidecar-proto/",
|
|
"e2e/ui/",
|
|
"e2e/lib/",
|
|
"e2e/resources/",
|
|
"e2e/scripts/",
|
|
".github/actions/setup-playwright/",
|
|
".github/actions/setup-workspace/",
|
|
]) ||
|
|
[
|
|
"e2e/package.json",
|
|
"e2e/playwright.config.ts",
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
".github/workflows/ci.yml",
|
|
".github/workflows/ui-extended-main.yml",
|
|
].includes(file)
|
|
);
|
|
}
|
|
|
|
function isVisualRelevantFile(file: string): boolean {
|
|
return (
|
|
startsWithAny(file, [
|
|
"apps/web/",
|
|
"e2e/lib/playwright/",
|
|
".github/actions/setup-playwright/",
|
|
".github/actions/setup-workspace/",
|
|
]) ||
|
|
/^e2e\/ui\/visual-[^/]+\.test\.ts$/.test(file) ||
|
|
[
|
|
"e2e/package.json",
|
|
"e2e/playwright.visual.config.ts",
|
|
"e2e/scripts/playwright.ts",
|
|
"e2e/scripts/visual-report.ts",
|
|
"pnpm-lock.yaml",
|
|
".github/scripts/handoff.py",
|
|
".github/workflows/ci.yml",
|
|
".github/workflows/comment.atom.yml",
|
|
".github/workflows/report.atom.yml",
|
|
".github/workflows/visual-baseline.yml",
|
|
].includes(file)
|
|
);
|
|
}
|
|
|
|
function isDockerRelevantFile(file: string): boolean {
|
|
return (
|
|
startsWithAny(file, [
|
|
"deploy/",
|
|
]) ||
|
|
[
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
".dockerignore",
|
|
".github/workflows/ci.yml",
|
|
".github/workflows/docker-image.yml",
|
|
].includes(file) ||
|
|
isWorkspaceManifestOrCiFile(file)
|
|
);
|
|
}
|
|
|
|
function isNixRelevantFile(file: string): boolean {
|
|
return (
|
|
startsWithAny(file, [
|
|
"nix/",
|
|
"packages/release/",
|
|
]) ||
|
|
[
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
"flake.nix",
|
|
"flake.lock",
|
|
".github/scripts/handoff.py",
|
|
".github/workflows/autofix.atom.yml",
|
|
".github/workflows/ci.yml",
|
|
".github/workflows/comment.atom.yml",
|
|
"scripts/update-nix-pnpm-deps-hash.ts",
|
|
].includes(file) ||
|
|
isWorkspaceManifestOrCiFile(file)
|
|
);
|
|
}
|
|
|
|
function isWorkspaceValidationExemptFile(file: string): boolean {
|
|
return (
|
|
isDocumentationOrMetadataFile(file) ||
|
|
startsWithAny(file, [
|
|
"apps/landing-page/",
|
|
"nix/",
|
|
".github/ISSUE_TEMPLATE/",
|
|
]) ||
|
|
[
|
|
"flake.nix",
|
|
"flake.lock",
|
|
".github/workflows/landing-page-ci.yml",
|
|
".github/workflows/landing-page-staging.yml",
|
|
".github/workflows/landing-page-production.yml",
|
|
".github/workflows/blog-indexing-on-deploy.yml",
|
|
".github/workflows/autofix.atom.yml",
|
|
".github/workflows/comment.atom.yml",
|
|
".github/workflows/report.atom.yml",
|
|
".github/workflows/docker-image.yml",
|
|
].includes(file)
|
|
);
|
|
}
|
|
|
|
function isDocumentationOrMetadataFile(file: string): boolean {
|
|
return (
|
|
/\.(?:md|mdx|txt)$/.test(file) ||
|
|
file === "LICENSE" ||
|
|
file === ".gitignore" ||
|
|
file === ".editorconfig" ||
|
|
startsWithAny(file, [".vscode/", ".idea/", "docs/"]) ||
|
|
file === ".github/CODEOWNERS"
|
|
);
|
|
}
|
|
|
|
function startsWithAny(value: string, prefixes: string[]): boolean {
|
|
return prefixes.some((prefix) => value.startsWith(prefix));
|
|
}
|
|
|
|
function allScopeOutputsTrue(value: ScopeOutputs): boolean {
|
|
return Object.values(value).every(Boolean);
|
|
}
|
|
|
|
function writeGithubOutputs(value: ScopePlan): void {
|
|
const lines = Object.entries(value).map(([key, output]) => `${key}=${formatOutput(output)}`);
|
|
console.log(lines.join("\n"));
|
|
const outputPath = process.env.GITHUB_OUTPUT;
|
|
if (outputPath != null && outputPath.length > 0) {
|
|
appendFileSync(outputPath, `${lines.join("\n")}\n`);
|
|
}
|
|
}
|
|
|
|
function formatOutput(value: boolean | string): string {
|
|
return typeof value === "boolean" ? (value ? "true" : "false") : value;
|
|
}
|
|
|
|
function requiredEnv(name: string): string {
|
|
const value = process.env[name];
|
|
if (value == null || value.length === 0) {
|
|
throw new Error(`${name} is required`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function printUsage(): void {
|
|
console.log(`Usage: node --experimental-strip-types scripts/scopes.ts <command>
|
|
|
|
Commands:
|
|
github-output Write validation scope outputs for GitHub Actions
|
|
print Print the validation scope plan as JSON
|
|
help Show this help
|
|
`);
|
|
}
|