Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { forkPaseoHomeMetadata, resolvePaseoHomePath } from "./paseo-home-fork";
|
||||
import { startIsolatedHostDaemon } from "./isolated-host-daemon";
|
||||
|
||||
export interface E2EWorker {
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
function resolveOptionalHome(value: string | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return null;
|
||||
return resolvePaseoHomePath(trimmed === "current" ? "~/.paseo" : trimmed);
|
||||
}
|
||||
|
||||
async function createFakeEditorBin(): Promise<string> {
|
||||
const binDir = await mkdtemp(path.join(tmpdir(), "paseo-e2e-editor-bin-"));
|
||||
let realGhPath = "";
|
||||
try {
|
||||
realGhPath = execSync("which gh").toString().trim();
|
||||
} catch {
|
||||
// The local GitHub fixture remains usable without a system gh binary.
|
||||
}
|
||||
|
||||
const fakeEditorSource = `#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const recordPath = process.env.PASEO_E2E_EDITOR_RECORD_PATH;
|
||||
if (recordPath) {
|
||||
fs.appendFileSync(recordPath, JSON.stringify({
|
||||
command: path.basename(process.argv[1]),
|
||||
args: process.argv.slice(2),
|
||||
cwd: process.cwd(),
|
||||
at: Date.now()
|
||||
}) + "\\n");
|
||||
}
|
||||
`;
|
||||
for (const editorCommand of ["cursor", "code"]) {
|
||||
const editorPath = path.join(binDir, editorCommand);
|
||||
await writeFile(editorPath, fakeEditorSource);
|
||||
await chmod(editorPath, 0o755);
|
||||
}
|
||||
|
||||
const fakeGhPath = path.join(binDir, "gh");
|
||||
const fakeGhSource = `#!/usr/bin/env node
|
||||
const { spawnSync } = require("child_process");
|
||||
const args = process.argv.slice(2);
|
||||
const fixtureRemote = "https://github.com/paseo-e2e/local-fixture.git";
|
||||
const origin = spawnSync("git", ["config", "--get", "remote.origin.url"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
}).stdout?.trim();
|
||||
|
||||
if (origin === fixtureRemote) {
|
||||
const command = args.slice(0, 2).join(" ");
|
||||
if (command === "auth status") process.exit(0);
|
||||
if (command === "repo view") {
|
||||
process.stdout.write(JSON.stringify({ owner: { login: "paseo-e2e" }, name: "local-fixture", parent: null }));
|
||||
process.exit(0);
|
||||
}
|
||||
if (command === "issue list") {
|
||||
process.stdout.write("[]");
|
||||
process.exit(0);
|
||||
}
|
||||
if (command === "pr list" || command === "pr view") {
|
||||
const pr = {
|
||||
number: 1,
|
||||
title: "Use pasted PR as start ref",
|
||||
url: "https://github.com/paseo-e2e/local-fixture/pull/1",
|
||||
state: "OPEN",
|
||||
body: null,
|
||||
labels: [],
|
||||
baseRefName: "main",
|
||||
headRefName: "pr-branch-1",
|
||||
updatedAt: "2026-01-01T00:00:00Z"
|
||||
};
|
||||
process.stdout.write(JSON.stringify(command === "pr list" ? [pr] : pr));
|
||||
process.exit(0);
|
||||
}
|
||||
if (command === "api graphql" && args.some((arg) => arg.includes("PullRequestCheckoutTarget"))) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
data: { repository: { pullRequest: {
|
||||
number: 1,
|
||||
baseRefName: "main",
|
||||
headRefName: "pr-branch-1",
|
||||
isCrossRepository: false,
|
||||
headRepositoryOwner: { login: "paseo-e2e" },
|
||||
headRepository: {
|
||||
sshUrl: "git@github.com:paseo-e2e/local-fixture.git",
|
||||
url: fixtureRemote
|
||||
}
|
||||
} } }
|
||||
}));
|
||||
process.exit(0);
|
||||
}
|
||||
process.stderr.write("Unsupported local GitHub fixture command: " + args.join(" ") + "\\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const realGhPath = ${JSON.stringify(realGhPath)};
|
||||
if (!realGhPath) process.exit(127);
|
||||
const result = spawnSync(realGhPath, args, { stdio: "inherit" });
|
||||
process.exit(result.status ?? 1);
|
||||
`;
|
||||
await writeFile(fakeGhPath, fakeGhSource);
|
||||
await chmod(fakeGhPath, 0o755);
|
||||
return binDir;
|
||||
}
|
||||
|
||||
async function applyMetadataFork(targetHome: string): Promise<void> {
|
||||
const sourceHome = resolveOptionalHome(process.env.E2E_FORK_PASEO_HOME_FROM);
|
||||
if (!sourceHome) return;
|
||||
const result = await forkPaseoHomeMetadata({ sourceHome, targetHome });
|
||||
process.env.E2E_FORK_SOURCE_PASEO_HOME = result.sourceHome;
|
||||
process.env.E2E_FORK_TARGET_PASEO_HOME = result.targetHome;
|
||||
process.env.E2E_FORK_COPIED_FILES = String(result.copiedFiles);
|
||||
process.env.E2E_FORK_COPIED_BYTES = String(result.copiedBytes);
|
||||
}
|
||||
|
||||
export async function startE2EWorker(workerIndex: number): Promise<E2EWorker> {
|
||||
const requestedRoot = resolveOptionalHome(process.env.E2E_PASEO_HOME);
|
||||
const paseoHome = requestedRoot
|
||||
? path.join(requestedRoot, `worker-${workerIndex}`)
|
||||
: await mkdtemp(path.join(tmpdir(), `paseo-e2e-worker-${workerIndex}-`));
|
||||
const preserveHome = Boolean(requestedRoot) || process.env.E2E_KEEP_PASEO_HOME === "1";
|
||||
const fakeEditorBin = await createFakeEditorBin();
|
||||
const editorRecordPath = path.join(paseoHome, "editor-open-records.jsonl");
|
||||
const serverId = `srv_e2e_worker_${workerIndex}`;
|
||||
|
||||
try {
|
||||
await applyMetadataFork(paseoHome);
|
||||
const daemon = await startIsolatedHostDaemon(serverId, {
|
||||
paseoHome,
|
||||
preserveHome,
|
||||
environment: {
|
||||
NODE_ENV: "development",
|
||||
PATH: `${fakeEditorBin}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
PASEO_E2E_EDITOR_RECORD_PATH: editorRecordPath,
|
||||
},
|
||||
});
|
||||
|
||||
process.env.E2E_DAEMON_PORT = String(daemon.port);
|
||||
process.env.E2E_SERVER_ID = daemon.serverId;
|
||||
process.env.E2E_PASEO_HOME = daemon.paseoHome;
|
||||
process.env.E2E_EDITOR_RECORD_PATH = editorRecordPath;
|
||||
delete process.env.E2E_RELAY_PORT;
|
||||
delete process.env.E2E_RELAY_DAEMON_PUBLIC_KEY;
|
||||
|
||||
console.log(
|
||||
`[e2e] Worker ${workerIndex} daemon started on port ${daemon.port}, home: ${daemon.paseoHome}`,
|
||||
);
|
||||
return {
|
||||
close: async () => {
|
||||
await daemon.close();
|
||||
await rm(fakeEditorBin, { recursive: true, force: true });
|
||||
console.log(`[e2e] Worker ${workerIndex} daemon stopped`);
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await rm(fakeEditorBin, { recursive: true, force: true });
|
||||
if (!preserveHome) await rm(paseoHome, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user