@pushpalsdev/cli 1.0.95 → 1.0.97
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
|
@@ -369,21 +369,41 @@ def _normalize_choice(
|
|
|
369
369
|
return default
|
|
370
370
|
|
|
371
371
|
|
|
372
|
-
def _is_git_repo(repo: str) -> bool:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
372
|
+
def _is_git_repo(repo: str, timeout_seconds: float = 5.0, poll_seconds: float = 0.1) -> bool:
|
|
373
|
+
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
|
374
|
+
last_detail = ""
|
|
375
|
+
attempts = 0
|
|
376
|
+
|
|
377
|
+
while True:
|
|
378
|
+
attempts += 1
|
|
379
|
+
try:
|
|
380
|
+
proc = subprocess.run(
|
|
381
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
382
|
+
cwd=repo,
|
|
383
|
+
capture_output=True,
|
|
384
|
+
text=True,
|
|
385
|
+
timeout=10,
|
|
386
|
+
check=False,
|
|
387
|
+
)
|
|
388
|
+
if proc.returncode == 0 and (proc.stdout or "").strip().lower() == "true":
|
|
389
|
+
return True
|
|
390
|
+
last_detail = "\n".join(
|
|
391
|
+
part.strip()
|
|
392
|
+
for part in [proc.stderr or "", proc.stdout or ""]
|
|
393
|
+
if part and part.strip()
|
|
394
|
+
)
|
|
395
|
+
except Exception as exc:
|
|
396
|
+
last_detail = str(exc)
|
|
397
|
+
|
|
398
|
+
if time.monotonic() >= deadline:
|
|
399
|
+
if last_detail:
|
|
400
|
+
log.warning(
|
|
401
|
+
"Git repository preflight failed "
|
|
402
|
+
f"after {attempts} attempt(s) for {repo}: {to_single_line(last_detail, 240)}"
|
|
403
|
+
)
|
|
383
404
|
return False
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return False
|
|
405
|
+
|
|
406
|
+
time.sleep(max(0.01, poll_seconds))
|
|
387
407
|
|
|
388
408
|
|
|
389
409
|
def _codex_project_config_roots(repo: str, env: Dict[str, str]) -> List[Path]:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
-
import { existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, tmpdir } from "os";
|
|
4
4
|
import { basename, resolve } from "path";
|
|
5
5
|
|
|
@@ -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;
|
|
@@ -33,6 +41,18 @@ function ensureDirs(paths: string[]): void {
|
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function ensureSandboxGitConfig(homeDir: string): void {
|
|
45
|
+
const gitConfigPath = resolve(homeDir, ".gitconfig");
|
|
46
|
+
try {
|
|
47
|
+
const existing = existsSync(gitConfigPath) ? readFileSync(gitConfigPath, "utf8") : "";
|
|
48
|
+
if (/(^|\n)\s*directory\s*=\s*\*/.test(existing)) return;
|
|
49
|
+
const prefix = existing.trim() ? `${existing.replace(/\s+$/, "")}\n\n` : "";
|
|
50
|
+
writeFileSync(gitConfigPath, `${prefix}[safe]\n\tdirectory = *\n`, "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
// Best effort: git will surface any remaining safe.directory blocker.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
function resolveOriginalHome(env: Record<string, string>): string {
|
|
37
57
|
return env.HOME || env.USERPROFILE || homedir();
|
|
38
58
|
}
|
|
@@ -54,8 +74,18 @@ export function buildWorkerSandboxWritableEnv(
|
|
|
54
74
|
const homeDir = resolve(baseDir, "home");
|
|
55
75
|
const cacheDir = resolve(baseDir, "cache");
|
|
56
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
|
+
);
|
|
57
86
|
const defaultExpoPort = defaultExpoPortForRepo(repo);
|
|
58
|
-
ensureDirs([homeDir, cacheDir, expoDir, resolve(cacheDir, "npm")]);
|
|
87
|
+
ensureDirs([homeDir, cacheDir, expoDir, resolve(cacheDir, "npm"), playwrightBrowsersDir]);
|
|
88
|
+
ensureSandboxGitConfig(homeDir);
|
|
59
89
|
|
|
60
90
|
return {
|
|
61
91
|
...env,
|
|
@@ -64,6 +94,7 @@ export function buildWorkerSandboxWritableEnv(
|
|
|
64
94
|
USERPROFILE: homeDir,
|
|
65
95
|
XDG_CACHE_HOME: cacheDir,
|
|
66
96
|
npm_config_cache: resolve(cacheDir, "npm"),
|
|
97
|
+
PLAYWRIGHT_BROWSERS_PATH: env.PLAYWRIGHT_BROWSERS_PATH ?? playwrightBrowsersDir,
|
|
67
98
|
EXPO_HOME: expoDir,
|
|
68
99
|
EXPO_NO_TELEMETRY: env.EXPO_NO_TELEMETRY ?? "1",
|
|
69
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,168 @@ 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
|
+
export function playwrightBrowserInstallArgv(): string[] {
|
|
887
|
+
return ["bunx", "playwright", "install", "chromium"];
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function runPlaywrightBrowserRuntimePreflight(
|
|
891
|
+
repo: string,
|
|
892
|
+
command: string,
|
|
893
|
+
timeoutMs: number,
|
|
894
|
+
outputPolicy: Partial<OutputCompactionPolicy>,
|
|
895
|
+
): Promise<ValidationExecutionResult> {
|
|
896
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
897
|
+
const timeout = Math.max(120_000, Math.min(600_000, timeoutMs));
|
|
898
|
+
return runValidationArgv(
|
|
899
|
+
repo,
|
|
900
|
+
command,
|
|
901
|
+
playwrightBrowserInstallArgv(),
|
|
902
|
+
env,
|
|
903
|
+
timeout,
|
|
904
|
+
outputPolicy,
|
|
905
|
+
`Browser runtime preflight timed out after ${timeout}ms while ensuring Playwright Chromium. Captured output is the process output emitted before PushPals terminated the installer process tree.`,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
684
909
|
export function resolveValidationCommandTimeoutMs(command: string, baseTimeoutMs: number): number {
|
|
685
910
|
const normalizedBase = Number.isFinite(Number(baseTimeoutMs))
|
|
686
911
|
? Math.max(1_000, Math.min(7_200_000, Math.floor(Number(baseTimeoutMs))))
|
|
@@ -927,12 +1152,16 @@ function detectValidationBlocker(runs: ValidationExecutionResult[]): ValidationB
|
|
|
927
1152
|
combined.includes("missing required tool") ||
|
|
928
1153
|
combined.includes("command not found") ||
|
|
929
1154
|
combined.includes("executable not found") ||
|
|
1155
|
+
combined.includes("browser runtime preflight failed") ||
|
|
1156
|
+
combined.includes("playwright install") ||
|
|
1157
|
+
combined.includes("executable doesn't exist") ||
|
|
1158
|
+
combined.includes("please run the following command to download new browsers") ||
|
|
930
1159
|
combined.includes("not recognized as an internal or external command")
|
|
931
1160
|
) {
|
|
932
1161
|
return {
|
|
933
1162
|
category: "environment",
|
|
934
1163
|
detail:
|
|
935
|
-
"Validation is blocked by missing required toolchain executables in the worker environment. Install/provision the missing tools or
|
|
1164
|
+
"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
1165
|
};
|
|
937
1166
|
}
|
|
938
1167
|
|
|
@@ -1121,6 +1350,10 @@ export function extractValidationFailureDigest(run: {
|
|
|
1121
1350
|
/\bFailed to resolve import\s+['"`][^'"`\r\n]+['"`][^\r\n]*/i,
|
|
1122
1351
|
/\bCould not resolve\s+['"`]?[^'"`\r\n]+['"`]?[^\r\n]*/i,
|
|
1123
1352
|
/\bModule not found[^\r\n]*/i,
|
|
1353
|
+
/\bbrowserType\.launch:[^\r\n]*/i,
|
|
1354
|
+
/\bExecutable doesn't exist[^\r\n]*/i,
|
|
1355
|
+
/\bPlease run the following command to download new browsers:[^\r\n]*(?:\r?\n\s+[^\r\n]+)?/i,
|
|
1356
|
+
/\bRun ["`]?npx playwright install[^'"`\r\n]*["`]?[^\r\n]*/i,
|
|
1124
1357
|
/\bERR_SOCKET_BAD_PORT[^\r\n]*/i,
|
|
1125
1358
|
/\berror TS\d+:[^\r\n]*/i,
|
|
1126
1359
|
/\bError:\s+[^\r\n]*/i,
|
|
@@ -1539,6 +1772,7 @@ async function runDeterministicQualityGate(
|
|
|
1539
1772
|
)}`,
|
|
1540
1773
|
);
|
|
1541
1774
|
}
|
|
1775
|
+
let playwrightBrowserRuntimeReady = false;
|
|
1542
1776
|
for (const command of commandsToRun) {
|
|
1543
1777
|
const commandMissingTools = requirementsForValidationCommand(toolchainPlan, command).filter(
|
|
1544
1778
|
(requirement) =>
|
|
@@ -1563,6 +1797,48 @@ async function runDeterministicQualityGate(
|
|
|
1563
1797
|
);
|
|
1564
1798
|
continue;
|
|
1565
1799
|
}
|
|
1800
|
+
const commandNeedsPlaywrightBrowserRuntime = shouldEnsurePlaywrightBrowserRuntime(
|
|
1801
|
+
repo,
|
|
1802
|
+
command,
|
|
1803
|
+
);
|
|
1804
|
+
let commandBrowserRuntimeEnsured =
|
|
1805
|
+
playwrightBrowserRuntimeReady && commandNeedsPlaywrightBrowserRuntime;
|
|
1806
|
+
if (!playwrightBrowserRuntimeReady && commandNeedsPlaywrightBrowserRuntime) {
|
|
1807
|
+
const browserEnv = buildWorkerSandboxWritableEnv(repo);
|
|
1808
|
+
onLog?.(
|
|
1809
|
+
"stdout",
|
|
1810
|
+
`[ValidationGate] Browser runtime preflight: ensuring Playwright Chromium for "${command}" at ${browserEnv.PLAYWRIGHT_BROWSERS_PATH ?? "(default browser cache)"}`,
|
|
1811
|
+
);
|
|
1812
|
+
const browserPreflight = await runPlaywrightBrowserRuntimePreflight(
|
|
1813
|
+
repo,
|
|
1814
|
+
command,
|
|
1815
|
+
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
1816
|
+
outputPolicy,
|
|
1817
|
+
);
|
|
1818
|
+
if (!browserPreflight.ok) {
|
|
1819
|
+
const digest = extractValidationFailureDigest(browserPreflight);
|
|
1820
|
+
validationRuns.push({
|
|
1821
|
+
...browserPreflight,
|
|
1822
|
+
stderr: [
|
|
1823
|
+
`Browser runtime preflight failed before validation command "${command}". WorkerPals could not ensure Playwright Chromium in PLAYWRIGHT_BROWSERS_PATH=${browserEnv.PLAYWRIGHT_BROWSERS_PATH ?? "(default)"}.`,
|
|
1824
|
+
browserPreflight.stderr,
|
|
1825
|
+
]
|
|
1826
|
+
.filter(Boolean)
|
|
1827
|
+
.join("\n"),
|
|
1828
|
+
});
|
|
1829
|
+
onLog?.(
|
|
1830
|
+
"stderr",
|
|
1831
|
+
`[ValidationGate] Browser runtime preflight failed for "${command}"${digest ? ` - ${digest}` : ""}`,
|
|
1832
|
+
);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
playwrightBrowserRuntimeReady = true;
|
|
1836
|
+
onLog?.(
|
|
1837
|
+
"stdout",
|
|
1838
|
+
`[ValidationGate] Browser runtime preflight passed for "${command}"`,
|
|
1839
|
+
);
|
|
1840
|
+
commandBrowserRuntimeEnsured = true;
|
|
1841
|
+
}
|
|
1566
1842
|
const previousDigest = validationRetryState?.previousFailureDigests?.get(
|
|
1567
1843
|
validationCommandKey(command),
|
|
1568
1844
|
);
|
|
@@ -1570,7 +1846,8 @@ async function runDeterministicQualityGate(
|
|
|
1570
1846
|
previousDigest &&
|
|
1571
1847
|
Number(validationRetryState?.revisionAttempt ?? 0) > 0 &&
|
|
1572
1848
|
isLongRunningBrowserValidationCommand(command) &&
|
|
1573
|
-
isBrowserValidationInfrastructureDigest(previousDigest)
|
|
1849
|
+
isBrowserValidationInfrastructureDigest(previousDigest) &&
|
|
1850
|
+
!commandBrowserRuntimeEnsured
|
|
1574
1851
|
) {
|
|
1575
1852
|
const stderr =
|
|
1576
1853
|
`Skipped repeated browser validation after the same command failed in an earlier revision: ${previousDigest}. ` +
|