@pushpalsdev/cli 1.0.96 → 1.0.98
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/package.json
CHANGED
|
@@ -17,6 +17,14 @@ function safeRepoSlug(repo: string): string {
|
|
|
17
17
|
return `${leaf}-${hash}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function browserCacheRepoKey(repo: string): string {
|
|
21
|
+
const normalized = resolve(repo).replace(/\\/g, "/");
|
|
22
|
+
const marker = "/.worktrees/";
|
|
23
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
24
|
+
if (markerIndex < 0) return resolve(repo);
|
|
25
|
+
return normalized.slice(0, markerIndex);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function defaultExpoPortForRepo(repo: string): string {
|
|
21
29
|
const hashPrefix = createHash("sha256").update(resolve(repo)).digest("hex").slice(0, 8);
|
|
22
30
|
const offset = Number.parseInt(hashPrefix, 16) % 1_000;
|
|
@@ -66,8 +74,17 @@ export function buildWorkerSandboxWritableEnv(
|
|
|
66
74
|
const homeDir = resolve(baseDir, "home");
|
|
67
75
|
const cacheDir = resolve(baseDir, "cache");
|
|
68
76
|
const expoDir = resolve(baseDir, "expo");
|
|
77
|
+
const playwrightBrowsersDir =
|
|
78
|
+
env.PLAYWRIGHT_BROWSERS_PATH && env.PLAYWRIGHT_BROWSERS_PATH !== "0"
|
|
79
|
+
? env.PLAYWRIGHT_BROWSERS_PATH
|
|
80
|
+
: resolve(
|
|
81
|
+
tmpdir(),
|
|
82
|
+
"pushpals-worker-env",
|
|
83
|
+
safeRepoSlug(browserCacheRepoKey(repo)),
|
|
84
|
+
"playwright-browsers",
|
|
85
|
+
);
|
|
69
86
|
const defaultExpoPort = defaultExpoPortForRepo(repo);
|
|
70
|
-
ensureDirs([homeDir, cacheDir, expoDir, resolve(cacheDir, "npm")]);
|
|
87
|
+
ensureDirs([homeDir, cacheDir, expoDir, resolve(cacheDir, "npm"), playwrightBrowsersDir]);
|
|
71
88
|
ensureSandboxGitConfig(homeDir);
|
|
72
89
|
|
|
73
90
|
return {
|
|
@@ -77,6 +94,7 @@ export function buildWorkerSandboxWritableEnv(
|
|
|
77
94
|
USERPROFILE: homeDir,
|
|
78
95
|
XDG_CACHE_HOME: cacheDir,
|
|
79
96
|
npm_config_cache: resolve(cacheDir, "npm"),
|
|
97
|
+
PLAYWRIGHT_BROWSERS_PATH: env.PLAYWRIGHT_BROWSERS_PATH ?? playwrightBrowsersDir,
|
|
80
98
|
EXPO_HOME: expoDir,
|
|
81
99
|
EXPO_NO_TELEMETRY: env.EXPO_NO_TELEMETRY ?? "1",
|
|
82
100
|
EXPO_NO_INTERACTIVE: env.EXPO_NO_INTERACTIVE ?? "1",
|
|
@@ -601,42 +601,75 @@ export function tokenizeValidationCommandArgv(command: string): string[] | null
|
|
|
601
601
|
return out;
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
async function
|
|
604
|
+
async function terminateValidationProcessTree(
|
|
605
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
606
|
+
): Promise<void> {
|
|
607
|
+
const pid = Number(proc.pid);
|
|
608
|
+
if (process.platform === "win32" && Number.isFinite(pid) && pid > 0) {
|
|
609
|
+
try {
|
|
610
|
+
Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
611
|
+
stdout: "pipe",
|
|
612
|
+
stderr: "pipe",
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
} catch {
|
|
616
|
+
// Fall through to Bun's process handle.
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (process.platform !== "win32" && Number.isFinite(pid) && pid > 0) {
|
|
621
|
+
try {
|
|
622
|
+
process.kill(-pid, "SIGTERM");
|
|
623
|
+
} catch {
|
|
624
|
+
try {
|
|
625
|
+
proc.kill("SIGTERM");
|
|
626
|
+
} catch {
|
|
627
|
+
// ignore
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
await Bun.sleep(2_000);
|
|
631
|
+
try {
|
|
632
|
+
process.kill(-pid, "SIGKILL");
|
|
633
|
+
} catch {
|
|
634
|
+
try {
|
|
635
|
+
proc.kill("SIGKILL");
|
|
636
|
+
} catch {
|
|
637
|
+
// ignore
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
proc.kill();
|
|
645
|
+
} catch {
|
|
646
|
+
// ignore
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function runValidationArgv(
|
|
605
651
|
repo: string,
|
|
606
652
|
command: string,
|
|
653
|
+
argv: string[],
|
|
654
|
+
env: Record<string, string>,
|
|
607
655
|
timeoutMs: number,
|
|
608
656
|
outputPolicy: Partial<OutputCompactionPolicy>,
|
|
657
|
+
timeoutMessage: string,
|
|
609
658
|
): Promise<ValidationExecutionResult> {
|
|
610
|
-
const env = buildWorkerSandboxWritableEnv(repo);
|
|
611
|
-
const argv = prepareValidationCommandArgv(command, env);
|
|
612
|
-
if (!argv) {
|
|
613
|
-
return {
|
|
614
|
-
step: command,
|
|
615
|
-
command,
|
|
616
|
-
ok: false,
|
|
617
|
-
exitCode: 2,
|
|
618
|
-
stdout: "",
|
|
619
|
-
stderr:
|
|
620
|
-
"Validation command could not be parsed safely. Use a plain command without shell chaining/pipes.",
|
|
621
|
-
elapsedMs: 1,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
659
|
const startedAt = Date.now();
|
|
625
660
|
const proc = Bun.spawn(argv, {
|
|
626
661
|
cwd: repo,
|
|
627
662
|
env,
|
|
663
|
+
stdin: "ignore",
|
|
628
664
|
stdout: "pipe",
|
|
629
665
|
stderr: "pipe",
|
|
666
|
+
detached: process.platform !== "win32",
|
|
630
667
|
});
|
|
631
668
|
let timedOut = false;
|
|
632
669
|
const timer = setTimeout(
|
|
633
670
|
() => {
|
|
634
671
|
timedOut = true;
|
|
635
|
-
|
|
636
|
-
proc.kill();
|
|
637
|
-
} catch {
|
|
638
|
-
// ignore
|
|
639
|
-
}
|
|
672
|
+
void terminateValidationProcessTree(proc);
|
|
640
673
|
},
|
|
641
674
|
Math.max(1_000, timeoutMs),
|
|
642
675
|
);
|
|
@@ -657,9 +690,7 @@ async function runValidationCommand(
|
|
|
657
690
|
stderr: compactJobOutput(
|
|
658
691
|
[
|
|
659
692
|
stderr.trim(),
|
|
660
|
-
timedOut
|
|
661
|
-
? `Validation command timed out after ${Math.max(1_000, timeoutMs)}ms. Captured output is the process output emitted before PushPals terminated the command.`
|
|
662
|
-
: "",
|
|
693
|
+
timedOut ? timeoutMessage : "",
|
|
663
694
|
]
|
|
664
695
|
.filter(Boolean)
|
|
665
696
|
.join("\n"),
|
|
@@ -669,6 +700,38 @@ async function runValidationCommand(
|
|
|
669
700
|
};
|
|
670
701
|
}
|
|
671
702
|
|
|
703
|
+
async function runValidationCommand(
|
|
704
|
+
repo: string,
|
|
705
|
+
command: string,
|
|
706
|
+
timeoutMs: number,
|
|
707
|
+
outputPolicy: Partial<OutputCompactionPolicy>,
|
|
708
|
+
): Promise<ValidationExecutionResult> {
|
|
709
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
710
|
+
const argv = prepareValidationCommandArgv(command, env);
|
|
711
|
+
if (!argv) {
|
|
712
|
+
return {
|
|
713
|
+
step: command,
|
|
714
|
+
command,
|
|
715
|
+
ok: false,
|
|
716
|
+
exitCode: 2,
|
|
717
|
+
stdout: "",
|
|
718
|
+
stderr:
|
|
719
|
+
"Validation command could not be parsed safely. Use a plain command without shell chaining/pipes.",
|
|
720
|
+
elapsedMs: 1,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return runValidationArgv(
|
|
725
|
+
repo,
|
|
726
|
+
command,
|
|
727
|
+
argv,
|
|
728
|
+
env,
|
|
729
|
+
timeoutMs,
|
|
730
|
+
outputPolicy,
|
|
731
|
+
`Validation command timed out after ${Math.max(1_000, timeoutMs)}ms. Captured output is the process output emitted before PushPals terminated the command and its process tree.`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
672
735
|
export function isLongRunningBrowserValidationCommand(command: string): boolean {
|
|
673
736
|
const normalized = validationCommandKey(command);
|
|
674
737
|
if (!normalized) return false;
|
|
@@ -681,6 +744,224 @@ export function isLongRunningBrowserValidationCommand(command: string): boolean
|
|
|
681
744
|
);
|
|
682
745
|
}
|
|
683
746
|
|
|
747
|
+
function readPackageJson(repo: string): {
|
|
748
|
+
scripts?: Record<string, unknown>;
|
|
749
|
+
dependencies?: Record<string, unknown>;
|
|
750
|
+
devDependencies?: Record<string, unknown>;
|
|
751
|
+
optionalDependencies?: Record<string, unknown>;
|
|
752
|
+
peerDependencies?: Record<string, unknown>;
|
|
753
|
+
} | null {
|
|
754
|
+
const packagePath = resolve(repo, "package.json");
|
|
755
|
+
if (!existsSync(packagePath)) return null;
|
|
756
|
+
try {
|
|
757
|
+
return JSON.parse(readFileSync(packagePath, "utf8"));
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function packageJsonDeclaresPlaywright(repo: string): boolean {
|
|
764
|
+
const parsed = readPackageJson(repo);
|
|
765
|
+
if (!parsed) return false;
|
|
766
|
+
const dependencyGroups = [
|
|
767
|
+
parsed.dependencies,
|
|
768
|
+
parsed.devDependencies,
|
|
769
|
+
parsed.optionalDependencies,
|
|
770
|
+
parsed.peerDependencies,
|
|
771
|
+
];
|
|
772
|
+
return dependencyGroups.some((group) =>
|
|
773
|
+
Boolean(group && (group.playwright || group["@playwright/test"])),
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function resolvePackageScriptForValidationCommand(
|
|
778
|
+
repo: string,
|
|
779
|
+
command: string,
|
|
780
|
+
): { script: string; cwd: string } | null {
|
|
781
|
+
const argv = tokenizeValidationCommandArgv(command);
|
|
782
|
+
if (!argv || argv.length === 0) return null;
|
|
783
|
+
const first = argv[0]?.toLowerCase();
|
|
784
|
+
let cwd = repo;
|
|
785
|
+
let scriptName = "";
|
|
786
|
+
|
|
787
|
+
const consumeCwdOption = (index: number): number | null => {
|
|
788
|
+
const token = argv[index] ?? "";
|
|
789
|
+
if ((token === "--cwd" || token === "-C" || token === "--prefix") && argv[index + 1]) {
|
|
790
|
+
cwd = resolve(repo, argv[index + 1] ?? "");
|
|
791
|
+
return index + 2;
|
|
792
|
+
}
|
|
793
|
+
for (const prefix of ["--cwd=", "-C=", "--prefix="]) {
|
|
794
|
+
if (token.startsWith(prefix)) {
|
|
795
|
+
cwd = resolve(repo, token.slice(prefix.length));
|
|
796
|
+
return index + 1;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
if (first === "bun") {
|
|
803
|
+
let index = 1;
|
|
804
|
+
while (index < argv.length) {
|
|
805
|
+
const consumed = consumeCwdOption(index);
|
|
806
|
+
if (consumed !== null) {
|
|
807
|
+
index = consumed;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if ((argv[index] ?? "").startsWith("--")) {
|
|
811
|
+
index += 1;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
if ((argv[index] ?? "").toLowerCase() === "run") {
|
|
817
|
+
scriptName = argv[index + 1] ?? "";
|
|
818
|
+
} else {
|
|
819
|
+
const candidate = argv[index] ?? "";
|
|
820
|
+
if (candidate && !["install", "test", "x"].includes(candidate.toLowerCase())) {
|
|
821
|
+
scriptName = candidate;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
} else if (first === "npm" || first === "pnpm" || first === "yarn") {
|
|
825
|
+
let index = 1;
|
|
826
|
+
while (index < argv.length) {
|
|
827
|
+
const consumed = consumeCwdOption(index);
|
|
828
|
+
if (consumed !== null) {
|
|
829
|
+
index = consumed;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if ((argv[index] ?? "").toLowerCase() === "run") {
|
|
833
|
+
scriptName = argv[index + 1] ?? "";
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
if (!(argv[index] ?? "").startsWith("-")) {
|
|
837
|
+
scriptName = argv[index] ?? "";
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
index += 1;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!scriptName) return null;
|
|
845
|
+
const script = readPackageJson(cwd)?.scripts?.[scriptName];
|
|
846
|
+
if (typeof script !== "string" || !script.trim()) return null;
|
|
847
|
+
return { script, cwd };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function readReferencedValidationScriptText(cwd: string, script: string): string {
|
|
851
|
+
const texts: string[] = [];
|
|
852
|
+
const tokens = tokenizeValidationCommandArgv(script) ?? script.split(/\s+/).filter(Boolean);
|
|
853
|
+
for (const rawToken of tokens) {
|
|
854
|
+
const token = rawToken
|
|
855
|
+
.trim()
|
|
856
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
857
|
+
.replace(/\\/g, "/");
|
|
858
|
+
if (!/\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/i.test(token)) continue;
|
|
859
|
+
if (token.includes("://") || token.includes("node_modules/")) continue;
|
|
860
|
+
const scriptPath = resolve(cwd, token);
|
|
861
|
+
if (!existsSync(scriptPath)) continue;
|
|
862
|
+
try {
|
|
863
|
+
texts.push(readFileSync(scriptPath, "utf8").slice(0, 64_000));
|
|
864
|
+
} catch {
|
|
865
|
+
// Best effort: the validation command will surface unreadable files.
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return texts.join("\n");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function shouldEnsurePlaywrightBrowserRuntime(repo: string, command: string): boolean {
|
|
872
|
+
if (!isLongRunningBrowserValidationCommand(command)) return false;
|
|
873
|
+
if (/\bplaywright\b/i.test(command)) return true;
|
|
874
|
+
|
|
875
|
+
const script = resolvePackageScriptForValidationCommand(repo, command);
|
|
876
|
+
const scriptCwd = script?.cwd ?? repo;
|
|
877
|
+
if (packageJsonDeclaresPlaywright(repo) || packageJsonDeclaresPlaywright(scriptCwd)) {
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
if (!script) return false;
|
|
881
|
+
return /(?:^|[^A-Za-z0-9_-])(?:@playwright\/test|playwright)(?:$|[^A-Za-z0-9_-])/i.test(
|
|
882
|
+
`${script.script}\n${readReferencedValidationScriptText(script.cwd, script.script)}`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const PLAYWRIGHT_BROWSER_INSTALL_TARGETS = new Set([
|
|
887
|
+
"chromium",
|
|
888
|
+
"chrome",
|
|
889
|
+
"chrome-beta",
|
|
890
|
+
"chrome-dev",
|
|
891
|
+
"chrome-canary",
|
|
892
|
+
"msedge",
|
|
893
|
+
"msedge-beta",
|
|
894
|
+
"msedge-dev",
|
|
895
|
+
"msedge-canary",
|
|
896
|
+
"firefox",
|
|
897
|
+
"webkit",
|
|
898
|
+
]);
|
|
899
|
+
|
|
900
|
+
function addPlaywrightInstallTarget(targets: Set<string>, rawValue: string): void {
|
|
901
|
+
const value = rawValue.trim().toLowerCase();
|
|
902
|
+
if (!value) return;
|
|
903
|
+
const normalized = value === "edge" ? "msedge" : value;
|
|
904
|
+
if (PLAYWRIGHT_BROWSER_INSTALL_TARGETS.has(normalized)) {
|
|
905
|
+
targets.add(normalized);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function inferPlaywrightBrowserInstallTargets(repo: string, command: string): string[] {
|
|
910
|
+
const targets = new Set<string>(["chromium"]);
|
|
911
|
+
const script = resolvePackageScriptForValidationCommand(repo, command);
|
|
912
|
+
const scriptText = script
|
|
913
|
+
? `${script.script}\n${readReferencedValidationScriptText(script.cwd, script.script)}`
|
|
914
|
+
: "";
|
|
915
|
+
const text = `${command}\n${scriptText}`;
|
|
916
|
+
|
|
917
|
+
for (const match of text.matchAll(/\bchannel\s*:\s*["'`]([^"'`]+)["'`]/gi)) {
|
|
918
|
+
addPlaywrightInstallTarget(targets, match[1] ?? "");
|
|
919
|
+
}
|
|
920
|
+
for (const match of text.matchAll(/\bbrowserName\s*:\s*["'`]([^"'`]+)["'`]/gi)) {
|
|
921
|
+
addPlaywrightInstallTarget(targets, match[1] ?? "");
|
|
922
|
+
}
|
|
923
|
+
for (const match of text.matchAll(/(?:^|\s)(?:--browser|--browser-name|--channel)[=\s]+["'`]?([A-Za-z0-9_-]+)/gi)) {
|
|
924
|
+
addPlaywrightInstallTarget(targets, match[1] ?? "");
|
|
925
|
+
}
|
|
926
|
+
for (const target of PLAYWRIGHT_BROWSER_INSTALL_TARGETS) {
|
|
927
|
+
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
928
|
+
if (new RegExp(`\\b${escaped}\\s*\\.\\s*launch\\b`, "i").test(text)) {
|
|
929
|
+
addPlaywrightInstallTarget(targets, target);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return Array.from(targets).sort((a, b) => {
|
|
934
|
+
if (a === "chromium") return -1;
|
|
935
|
+
if (b === "chromium") return 1;
|
|
936
|
+
return a.localeCompare(b);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function playwrightBrowserInstallArgv(targets: string[] = ["chromium"]): string[] {
|
|
941
|
+
const installTargets = Array.from(new Set(targets.map((target) => target.trim()).filter(Boolean)));
|
|
942
|
+
return ["bunx", "playwright", "install", ...(installTargets.length > 0 ? installTargets : ["chromium"])];
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function runPlaywrightBrowserRuntimePreflight(
|
|
946
|
+
repo: string,
|
|
947
|
+
command: string,
|
|
948
|
+
targets: string[],
|
|
949
|
+
timeoutMs: number,
|
|
950
|
+
outputPolicy: Partial<OutputCompactionPolicy>,
|
|
951
|
+
): Promise<ValidationExecutionResult> {
|
|
952
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
953
|
+
const timeout = Math.max(120_000, Math.min(600_000, timeoutMs));
|
|
954
|
+
return runValidationArgv(
|
|
955
|
+
repo,
|
|
956
|
+
command,
|
|
957
|
+
playwrightBrowserInstallArgv(targets),
|
|
958
|
+
env,
|
|
959
|
+
timeout,
|
|
960
|
+
outputPolicy,
|
|
961
|
+
`Browser runtime preflight timed out after ${timeout}ms while ensuring Playwright browser target(s): ${targets.join(", ")}. Captured output is the process output emitted before PushPals terminated the installer process tree.`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
684
965
|
export function resolveValidationCommandTimeoutMs(command: string, baseTimeoutMs: number): number {
|
|
685
966
|
const normalizedBase = Number.isFinite(Number(baseTimeoutMs))
|
|
686
967
|
? Math.max(1_000, Math.min(7_200_000, Math.floor(Number(baseTimeoutMs))))
|
|
@@ -927,12 +1208,16 @@ function detectValidationBlocker(runs: ValidationExecutionResult[]): ValidationB
|
|
|
927
1208
|
combined.includes("missing required tool") ||
|
|
928
1209
|
combined.includes("command not found") ||
|
|
929
1210
|
combined.includes("executable not found") ||
|
|
1211
|
+
combined.includes("browser runtime preflight failed") ||
|
|
1212
|
+
combined.includes("playwright install") ||
|
|
1213
|
+
combined.includes("executable doesn't exist") ||
|
|
1214
|
+
combined.includes("please run the following command to download new browsers") ||
|
|
930
1215
|
combined.includes("not recognized as an internal or external command")
|
|
931
1216
|
) {
|
|
932
1217
|
return {
|
|
933
1218
|
category: "environment",
|
|
934
1219
|
detail:
|
|
935
|
-
"Validation is blocked by missing required toolchain executables in the worker environment. Install/provision the missing tools or
|
|
1220
|
+
"Validation is blocked by missing required toolchain executables or browser runtime support in the worker environment. Install/provision the missing tools or browser runtime before retrying this job.",
|
|
936
1221
|
};
|
|
937
1222
|
}
|
|
938
1223
|
|
|
@@ -1121,6 +1406,10 @@ export function extractValidationFailureDigest(run: {
|
|
|
1121
1406
|
/\bFailed to resolve import\s+['"`][^'"`\r\n]+['"`][^\r\n]*/i,
|
|
1122
1407
|
/\bCould not resolve\s+['"`]?[^'"`\r\n]+['"`]?[^\r\n]*/i,
|
|
1123
1408
|
/\bModule not found[^\r\n]*/i,
|
|
1409
|
+
/\bbrowserType\.launch:[^\r\n]*/i,
|
|
1410
|
+
/\bExecutable doesn't exist[^\r\n]*/i,
|
|
1411
|
+
/\bPlease run the following command to download new browsers:[^\r\n]*(?:\r?\n\s+[^\r\n]+)?/i,
|
|
1412
|
+
/\bRun ["`]?npx playwright install[^'"`\r\n]*["`]?[^\r\n]*/i,
|
|
1124
1413
|
/\bERR_SOCKET_BAD_PORT[^\r\n]*/i,
|
|
1125
1414
|
/\berror TS\d+:[^\r\n]*/i,
|
|
1126
1415
|
/\bError:\s+[^\r\n]*/i,
|
|
@@ -1539,6 +1828,7 @@ async function runDeterministicQualityGate(
|
|
|
1539
1828
|
)}`,
|
|
1540
1829
|
);
|
|
1541
1830
|
}
|
|
1831
|
+
const playwrightBrowserRuntimeReadyTargets = new Set<string>();
|
|
1542
1832
|
for (const command of commandsToRun) {
|
|
1543
1833
|
const commandMissingTools = requirementsForValidationCommand(toolchainPlan, command).filter(
|
|
1544
1834
|
(requirement) =>
|
|
@@ -1563,6 +1853,58 @@ async function runDeterministicQualityGate(
|
|
|
1563
1853
|
);
|
|
1564
1854
|
continue;
|
|
1565
1855
|
}
|
|
1856
|
+
const commandNeedsPlaywrightBrowserRuntime = shouldEnsurePlaywrightBrowserRuntime(
|
|
1857
|
+
repo,
|
|
1858
|
+
command,
|
|
1859
|
+
);
|
|
1860
|
+
const playwrightBrowserTargets = commandNeedsPlaywrightBrowserRuntime
|
|
1861
|
+
? inferPlaywrightBrowserInstallTargets(repo, command)
|
|
1862
|
+
: [];
|
|
1863
|
+
const missingPlaywrightBrowserTargets = playwrightBrowserTargets.filter(
|
|
1864
|
+
(target) => !playwrightBrowserRuntimeReadyTargets.has(target),
|
|
1865
|
+
);
|
|
1866
|
+
let commandBrowserRuntimeEnsured =
|
|
1867
|
+
commandNeedsPlaywrightBrowserRuntime &&
|
|
1868
|
+
missingPlaywrightBrowserTargets.length === 0;
|
|
1869
|
+
if (missingPlaywrightBrowserTargets.length > 0) {
|
|
1870
|
+
const browserEnv = buildWorkerSandboxWritableEnv(repo);
|
|
1871
|
+
onLog?.(
|
|
1872
|
+
"stdout",
|
|
1873
|
+
`[ValidationGate] Browser runtime preflight: ensuring Playwright browser target(s) ${missingPlaywrightBrowserTargets.join(", ")} for "${command}" at ${browserEnv.PLAYWRIGHT_BROWSERS_PATH ?? "(default browser cache)"}`,
|
|
1874
|
+
);
|
|
1875
|
+
const browserPreflight = await runPlaywrightBrowserRuntimePreflight(
|
|
1876
|
+
repo,
|
|
1877
|
+
command,
|
|
1878
|
+
missingPlaywrightBrowserTargets,
|
|
1879
|
+
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
1880
|
+
outputPolicy,
|
|
1881
|
+
);
|
|
1882
|
+
if (!browserPreflight.ok) {
|
|
1883
|
+
const digest = extractValidationFailureDigest(browserPreflight);
|
|
1884
|
+
validationRuns.push({
|
|
1885
|
+
...browserPreflight,
|
|
1886
|
+
stderr: [
|
|
1887
|
+
`Browser runtime preflight failed before validation command "${command}". WorkerPals could not ensure Playwright browser target(s) ${missingPlaywrightBrowserTargets.join(", ")} in PLAYWRIGHT_BROWSERS_PATH=${browserEnv.PLAYWRIGHT_BROWSERS_PATH ?? "(default)"}.`,
|
|
1888
|
+
browserPreflight.stderr,
|
|
1889
|
+
]
|
|
1890
|
+
.filter(Boolean)
|
|
1891
|
+
.join("\n"),
|
|
1892
|
+
});
|
|
1893
|
+
onLog?.(
|
|
1894
|
+
"stderr",
|
|
1895
|
+
`[ValidationGate] Browser runtime preflight failed for "${command}"${digest ? ` - ${digest}` : ""}`,
|
|
1896
|
+
);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
for (const target of missingPlaywrightBrowserTargets) {
|
|
1900
|
+
playwrightBrowserRuntimeReadyTargets.add(target);
|
|
1901
|
+
}
|
|
1902
|
+
onLog?.(
|
|
1903
|
+
"stdout",
|
|
1904
|
+
`[ValidationGate] Browser runtime preflight passed for "${command}" (${missingPlaywrightBrowserTargets.join(", ")})`,
|
|
1905
|
+
);
|
|
1906
|
+
commandBrowserRuntimeEnsured = true;
|
|
1907
|
+
}
|
|
1566
1908
|
const previousDigest = validationRetryState?.previousFailureDigests?.get(
|
|
1567
1909
|
validationCommandKey(command),
|
|
1568
1910
|
);
|
|
@@ -1570,7 +1912,8 @@ async function runDeterministicQualityGate(
|
|
|
1570
1912
|
previousDigest &&
|
|
1571
1913
|
Number(validationRetryState?.revisionAttempt ?? 0) > 0 &&
|
|
1572
1914
|
isLongRunningBrowserValidationCommand(command) &&
|
|
1573
|
-
isBrowserValidationInfrastructureDigest(previousDigest)
|
|
1915
|
+
isBrowserValidationInfrastructureDigest(previousDigest) &&
|
|
1916
|
+
!commandBrowserRuntimeEnsured
|
|
1574
1917
|
) {
|
|
1575
1918
|
const stderr =
|
|
1576
1919
|
`Skipped repeated browser validation after the same command failed in an earlier revision: ${previousDigest}. ` +
|