Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dist/index.mjs

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions src/run-install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";

// Mock the external shells before importing the SUT so its in-file references
// resolve to the mocked versions (same pattern as install-sfw.test.ts).
vi.mock("@actions/core", () => ({
startGroup: vi.fn(),
endGroup: vi.fn(),
setFailed: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
}));
vi.mock("@actions/exec", () => ({
getExecOutput: vi.fn(),
}));

import { setFailed, warning } from "@actions/core";
import { getExecOutput } from "@actions/exec";
import { isSfwVpNotFoundFlake, runViteInstall } from "./run-install.js";
import type { Inputs } from "./types.js";

// Real stderr from sfw-free v1.15.0 when its 10s PowerShell resolution
// times out on Windows (see voidzero-dev/setup-vp: sfw Windows flake).
const SFW_NOT_FOUND_STDERR = `Command 'vp' not found in PATH.

Possible solutions:
- Verify the command is installed
- Check it's in your PATH by running: where vp
- Try the full command name with extension (e.g., vp.cmd) - set SFW_DEBUG=true flag for more info.`;

const baseInputs: Inputs = {
version: "latest",
runInstall: [{}],
sfw: true,
cache: false,
};

const mockedExec = vi.mocked(getExecOutput);

function execResult(exitCode: number, stderr = "", stdout = "") {
return { exitCode, stdout, stderr };
}

const originalPlatform = process.platform;

function stubPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", { value: platform, configurable: true });
}

describe("isSfwVpNotFoundFlake", () => {
it("matches the sfw not-found signature in stderr", () => {
expect(isSfwVpNotFoundFlake("", SFW_NOT_FOUND_STDERR)).toBe(true);
});

it("matches the signature in stdout", () => {
expect(isSfwVpNotFoundFlake(SFW_NOT_FOUND_STDERR, "")).toBe(true);
});

it("does not match other failures", () => {
expect(isSfwVpNotFoundFlake("", "ERR_PNPM_FETCH_404 not found")).toBe(false);
expect(isSfwVpNotFoundFlake("", "Command 'pnpm' not found in PATH.")).toBe(false);
expect(isSfwVpNotFoundFlake("", "")).toBe(false);
});
});

describe("runViteInstall sfw retry", () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
});

it("runs once and succeeds without retry", async () => {
mockedExec.mockResolvedValueOnce(execResult(0));

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(1);
expect(setFailed).not.toHaveBeenCalled();
});

it("warms PowerShell and retries once on the not-found flake (win32)", async () => {
stubPlatform("win32");
mockedExec
.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR))
.mockResolvedValueOnce(execResult(0)) // warm-up
.mockResolvedValueOnce(execResult(0)); // retry

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(3);
expect(mockedExec.mock.calls[0]?.[0]).toBe("sfw");
expect(mockedExec.mock.calls[1]?.[0]).toBe("powershell.exe");
expect(mockedExec.mock.calls[1]?.[1]).toEqual(["-NoProfile", "-Command", "Get-Command vp"]);
expect(mockedExec.mock.calls[2]?.[0]).toBe("sfw");
expect(mockedExec.mock.calls[2]?.[1]).toEqual(["vp", "install"]);
expect(warning).toHaveBeenCalledTimes(1);
expect(setFailed).not.toHaveBeenCalled();
});

it("retries without a warm-up on non-Windows platforms", async () => {
stubPlatform("linux");
mockedExec
.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR))
.mockResolvedValueOnce(execResult(0)); // retry

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(2);
expect(mockedExec.mock.calls[1]?.[0]).toBe("sfw");
expect(setFailed).not.toHaveBeenCalled();
});

it("fails without retry when the failure does not match the signature", async () => {
stubPlatform("win32");
mockedExec.mockResolvedValueOnce(execResult(1, "ERR_PNPM_FETCH_404 not found"));

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(1);
expect(setFailed).toHaveBeenCalledTimes(1);
});

it("does not retry when sfw is disabled", async () => {
stubPlatform("win32");
mockedExec.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR));

await runViteInstall({ ...baseInputs, sfw: false });

expect(mockedExec).toHaveBeenCalledTimes(1);
expect(setFailed).toHaveBeenCalledTimes(1);
});

it("retries only once and fails when the flake persists", async () => {
stubPlatform("win32");
mockedExec
.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR))
.mockResolvedValueOnce(execResult(0)) // warm-up
.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR)); // retry fails too

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(3);
expect(setFailed).toHaveBeenCalledTimes(1);
});

it("still retries when the warm-up itself throws", async () => {
stubPlatform("win32");
mockedExec
.mockResolvedValueOnce(execResult(1, SFW_NOT_FOUND_STDERR))
.mockRejectedValueOnce(new Error("spawn powershell.exe ENOENT")) // warm-up
.mockResolvedValueOnce(execResult(0)); // retry

await runViteInstall(baseInputs);

expect(mockedExec).toHaveBeenCalledTimes(3);
expect(setFailed).not.toHaveBeenCalled();
});
});
66 changes: 55 additions & 11 deletions src/run-install.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import { startGroup, endGroup, setFailed, info, error as logError } from "@actions/core";
import { startGroup, endGroup, setFailed, info, warning, error as logError } from "@actions/core";
import { getExecOutput } from "@actions/exec";
import type { Inputs } from "./types.js";
import { getConfiguredProjectDir, getInstallCwd } from "./utils.js";

const MAX_ERROR_TAIL = 4000;

// sfw resolves the wrapped command on Windows by shelling out to
// `powershell.exe Get-Command` under a hard 10s child-process timeout
// (sfw-free v1.15.0, resolveWindowsCommand). A cold PowerShell start can
// exceed that, and sfw reports the killed lookup as "Command 'vp' not found
// in PATH" even though vp is installed. A genuine not-found returns in ~1s,
// so this signature on a wrapped install is near-certainly the timeout flake.
const SFW_VP_NOT_FOUND_RE = /Command 'vp' not found in PATH/;

export function isSfwVpNotFoundFlake(stdout: string, stderr: string): boolean {
return SFW_VP_NOT_FOUND_RE.test(stderr) || SFW_VP_NOT_FOUND_RE.test(stdout);
}

// Absorb the PowerShell cold start (assembly JIT + Get-Command module
// analysis over PSModulePath) with an uncapped lookup so sfw's retried
// 10s-limited resolution runs against warm caches. Best effort: a failure
// here must not block the retry.
async function warmPowerShellCommandCache(): Promise<void> {
if (process.platform !== "win32") return;
try {
await getExecOutput("powershell.exe", ["-NoProfile", "-Command", "Get-Command vp"], {
ignoreReturnCode: true,
});
Comment on lines +27 to +29
} catch (error) {
info(`PowerShell warm-up failed (${String(error)}); retrying sfw anyway.`);
}
}

function tailOutput(buffer: string, max: number): string {
const trimmed = buffer.trim();
if (trimmed.length <= max) return trimmed;
Expand All @@ -25,29 +52,46 @@ export async function runViteInstall(inputs: Inputs): Promise<void> {
const cwd = getInstallCwd(projectDir, options.cwd);
const cmdStr = `${cmd} ${args.join(" ")}`;

startGroup(`Running ${cmdStr} in ${cwd}...`);
const attempt = async (label: string) => {
startGroup(`Running ${label} in ${cwd}...`);
try {
return await getExecOutput(cmd, args, {
cwd,
ignoreReturnCode: true,
});
} finally {
endGroup();
}
};

try {
const { exitCode, stdout, stderr } = await getExecOutput(cmd, args, {
cwd,
ignoreReturnCode: true,
});
endGroup();
let result = await attempt(cmdStr);

if (
result.exitCode !== 0 &&
inputs.sfw &&
isSfwVpNotFoundFlake(result.stdout, result.stderr)
) {
warning(
"sfw reported vp as not found even though it is on PATH. This is a known sfw flake on Windows: a cold PowerShell start exceeds sfw's 10s command-resolution timeout and the timeout is misreported as not-found. Warming the PowerShell command cache and retrying once.",
);
Comment on lines +75 to +77
await warmPowerShellCommandCache();
result = await attempt(`${cmdStr} (retry)`);
}

if (exitCode === 0) {
if (result.exitCode === 0) {
info(`Successfully ran ${cmdStr}`);
continue;
}

const detail = stderr.trim() || stdout.trim();
const detail = result.stderr.trim() || result.stdout.trim();
if (detail) {
logError(tailOutput(detail, MAX_ERROR_TAIL), {
title: `${cmdStr} failed`,
});
}
setFailed(`Command "${cmdStr}" (cwd: ${cwd}) exited with code ${exitCode}`);
setFailed(`Command "${cmdStr}" (cwd: ${cwd}) exited with code ${result.exitCode}`);
} catch (error) {
endGroup();
setFailed(`Failed to run ${cmdStr}: ${String(error)}`);
}
}
Expand Down
Loading