Skip to content
Closed
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
54 changes: 54 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,60 @@ jobs:
assert.equal(node.mode, 'managed', 'Node.js management must remain enabled');
NODE

- name: Verify command resolution without and with system package managers
shell: bash
env:
EXPECTED_MODES: ${{ matrix.config.modes }}
run: |
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { delimiter, join } from 'node:path';

const managers = ['npm', 'pnpm', 'yarn', 'bun'];
const modes = JSON.parse(process.env.EXPECTED_MODES);
const windows = process.platform === 'win32';
const dirs = Object.fromEntries(execFileSync('vp', [], {
env: { ...process.env, VP_DUMP_DIRS: '1' }, encoding: 'utf8',
}).trim().split(/\r?\n/).map(line => line.split('\t')));
const system = mkdtempSync(join(tmpdir(), 'setup-vp-system-'));
const command = manager => windows ? `${manager}.exe` : manager;
const version = (manager, path = process.env.PATH) => execFileSync(command(manager), ['--version'], {
env: { ...process.env, PATH: path }, cwd: system, encoding: 'utf8',
}).trim();
try {
// Preserve the actual exported entries and ordering; do not manufacture the missing fallback.
const shimDirs = [dirs.bin, join(dirs.data, 'fallback-bin')];
const isolated = process.env.PATH.split(delimiter).filter(entry => shimDirs.includes(entry));
if (windows) {
isolated.push(join(process.env.SystemRoot, 'System32'));
} else {
// pnpm's native launcher needs these utilities, but no system package managers.
const utilities = join(system, 'utilities');
mkdirSync(utilities);
for (const name of ['sed', 'dirname', 'uname']) {
const executable = execFileSync('/bin/sh', ['-c', `command -v ${name}`], { encoding: 'utf8' }).trim();
symlinkSync(executable, join(utilities, name));
}
isolated.push(utilities);
}
for (const [index, manager] of managers.entries()) {
const managedVersion = version(manager, isolated.join(delimiter));
assert.ok(managedVersion, `${manager} must have a managed fallback`);
// A copy of Node acts as a native system executable on every OS.
copyFileSync(process.execPath, join(system, command(manager)));
const withSystem = [dirs.bin, system, ...isolated.slice(1)].join(delimiter);
assert.equal(version(manager, withSystem), modes[index] === 'managed' ? managedVersion : process.version);
const laterAddition = [system, process.env.PATH].join(delimiter);
assert.equal(version(manager, laterAddition), process.version, 'later PATH additions must win');
}
} finally {
rmSync(system, { recursive: true, force: true });
}
NODE

test-default-version:
# End-to-end check of the default version-resolution logic: with no `version`
# or `version-file` input, the action auto-detects the vite-plus version from
Expand Down
21 changes: 19 additions & 2 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions dist/index.mjs

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions src/azure/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { runFinalize, runPrepare } from "./index.js";
describe("Azure lifecycle", () => {
it("runs prepare in install → cache order", async () => {
const calls: string[] = [];
const setVariable = vi.fn();
const prependPath = vi.fn();
await runPrepare(
{
SETUP_VP_VERSION: "latest",
SETUP_VP_CACHE: "true",
SYSTEM_DEFAULTWORKINGDIRECTORY: process.cwd(),
},
{
installVitePlus: async () => {
installVitePlus: async (_version, options) => {
calls.push("install");
options?.exportPath?.("/bin:/system:/fallback");
options?.prependPath?.("/bin");
},
prepareCacheMetadata: () => {
calls.push("cache");
Expand All @@ -25,14 +29,16 @@ describe("Azure lifecycle", () => {
getCommandOutput: () => "vp v0.2.2",
run: () => undefined,
parseInstalledVpVersion: () => "0.2.2",
prependPath: () => undefined,
setVariable: () => undefined,
prependPath,
setVariable,
logWarning: () => undefined,
logInfo: () => undefined,
},
);

expect(calls).toEqual(["install", "cache"]);
expect(setVariable).toHaveBeenCalledWith("PATH", "/bin:/system:/fallback");
expect(prependPath).toHaveBeenCalledExactlyOnceWith("/bin");
});

it("runs finalize in auth → sfw → install → version order", async () => {
Expand Down
1 change: 1 addition & 0 deletions src/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export async function runPrepare(
await ports.installVitePlus(version, {
env,
prependPath: (binDir) => ports.prependPath(binDir),
exportPath: (value) => ports.setVariable("PATH", value),
logWarningFn: ports.logWarning,
});
// Finalize must use the resolved version, including pins from files or lockfiles.
Expand Down
27 changes: 27 additions & 0 deletions src/azure/install-viteplus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ function writeDirsFile(env: Record<string, string>, bin: string): void {
}

describe("installVitePlus", () => {
it.each(["linux", "win32"] as const)(
"propagates generated %s PATH even when main bin is already present",
async (platform) => {
const separator = platform === "win32" ? ";" : ":";
const bin = platform === "win32" ? "C:\\custom bin" : "/custom bin";
const originalPath = [bin, "system"].join(separator);
const installedPath = [originalPath, "fallback"].join(separator);
const env = { PATH: originalPath };
const exportPath = vi.fn();
const prependPath = vi.fn();
await installVitePlus("0.3.3", {
platform,
env,
exportPath,
prependPath,
runInstall: (_url, installEnv) => {
writeDirsFile(installEnv, bin);
writeFileSync(installEnv.SETUP_VP_DIRS_FILE, `\npath\t${installedPath}\n`, { flag: "a" });
return 0;
},
});
expect(env.PATH).toBe(installedPath);
expect(exportPath).toHaveBeenCalledExactlyOnceWith(installedPath);
expect(prependPath).toHaveBeenCalledExactlyOnceWith(bin);
},
);

it("uses PowerShell installers on Windows and bash installers on Unix", async () => {
const calls: NodeJS.Platform[] = [];
const runInstall = vi.fn(
Expand Down
78 changes: 78 additions & 0 deletions src/ci/install-path.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vite-plus/test";
import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { installVitePlus } from "./install-viteplus.js";
import { isWindows } from "./platform.js";
import { exportShellEnv, shellQuote } from "../gitlab/shell.js";

describe("installed PATH", () => {
it.skipIf(isWindows())(
"resolves managed and system-first tools now and in later GitLab shells",
async () => {
const root = mkdtempSync(join(tmpdir(), "setup-vp-path-"));
const bin = join(root, "custom bin");
const data = join(root, "data");
const config = join(root, "config");
const fallback = join(data, "fallback-bin");
const system = join(root, "system");
const downloads = join(root, "downloads");
const later = join(root, "later");
const envFile = join(root, "exports");
for (const dir of [bin, data, config, fallback, system, downloads, later])
mkdirSync(dir, { recursive: true });
const executable = (dir: string, name: string, body: string) => {
const file = join(dir, name);
writeFileSync(file, `#!/bin/bash\n${body}\n`);
chmodSync(file, 0o755);
};
try {
// Serve an isolated installer and generated environment, without downloading or touching a real installation.
const installer = join(root, "install.sh");
writeFileSync(installer, `SHIM_DIR=${shellQuote(bin)}\n`);
executable(downloads, "curl", `cp ${shellQuote(installer)} "$8"`);
executable(
bin,
"vp",
`if [ "$VP_DUMP_DIRS" = 1 ]; then\nprintf '%s\\n' ${["data\t" + data, "bin\t" + bin, "cache\t" + root, "config\t" + config, "state\t" + root].map(shellQuote).join(" ")}\nelse echo 'vp v0.3.3'; fi`,
);
writeFileSync(
join(config, "env"),
`export PATH=${shellQuote(bin)}:"$PATH":${shellQuote(fallback)}\n`,
);
executable(bin, "npm", "echo managed-npm");
executable(bin, "pnpm", "echo managed-pnpm");
executable(fallback, "pnpm", "echo fallback-pnpm");
const env = { HOME: root, PATH: `${downloads}:/usr/bin:/bin`, SETUP_VP_ENV_FILE: envFile };
await installVitePlus("0.3.3", {
env,
exportPath: (value) => exportShellEnv("PATH", value, env),
});
const resolve = (command: string, persisted = false) => {
const result = spawnSync(
"/bin/bash",
["-c", `${persisted ? `source ${shellQuote(envFile)}; ` : ""}${command}`],
{ env: persisted ? { HOME: root, PATH: "/usr/bin:/bin" } : env, encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(0);
return result.stdout.trim();
};
for (const persisted of [false, true])
expect(resolve("pnpm", persisted)).toBe("managed-pnpm");
// Model env off pnpm after setup: the shim leaves main bin; other families stay managed.
rmSync(join(bin, "pnpm"));
executable(system, "pnpm", "echo system-pnpm");
executable(later, "pnpm", "echo later-pnpm");
for (const persisted of [false, true]) {
expect(resolve("pnpm", persisted)).toBe("fallback-pnpm");
expect(resolve("npm", persisted)).toBe("managed-npm");
expect(resolve(`PATH=${shellQuote(system)}:"$PATH" pnpm`, persisted)).toBe("system-pnpm");
expect(resolve(`PATH=${shellQuote(later)}:"$PATH" pnpm`, persisted)).toBe("later-pnpm");
}
} finally {
rmSync(root, { recursive: true, force: true });
}
},
);
});
10 changes: 10 additions & 0 deletions src/ci/install-viteplus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createVitePlusDirsFile,
getInstallScriptCommand,
removeVitePlusDirsFile,
readVitePlusPath,
resolveVitePlusBinDir,
supportsVitePlusDirs,
VP_DIRS_FILE_ENV,
Expand Down Expand Up @@ -53,6 +54,7 @@ export async function installVitePlus(
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
prependPath?: (binDir: string) => void;
exportPath?: (value: string) => void;
sleep?: (ms: number) => Promise<void>;
runInstall?: typeof runInstallCommand;
logWarningFn?: (message: string) => void;
Expand Down Expand Up @@ -128,9 +130,17 @@ export async function installVitePlus(
dirsFile,
join(getVitePlusHome(platform, targetEnv), "bin"),
);
const installedPath = readVitePlusPath(dirsFile);
if (installedPath !== undefined) {
targetEnv.PATH = installedPath;
options.exportPath?.(installedPath);
prependPath?.(binDir);
return;
}
const separator = isWindows(platform) ? ";" : ":";
if (!targetEnv.PATH?.split(separator).includes(binDir)) {
targetEnv.PATH = `${binDir}${separator}${targetEnv.PATH || ""}`;
options.exportPath?.(targetEnv.PATH);
prependPath?.(binDir);
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/ci/vp-dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export function readVitePlusDirs(filePath: string): VitePlusDirs | undefined {
return output === undefined ? undefined : parseVitePlusDirs(output);
}

export function readVitePlusPath(filePath: string | undefined): string | undefined {
if (!filePath) return undefined;
return readVitePlusProbe(filePath)
?.split(/\r?\n/)
.find((line) => line.startsWith("path\t"))
?.slice(5);
}

function readVitePlusProbe(filePath: string): string | undefined {
try {
return readFileSync(filePath, "utf8");
Expand Down Expand Up @@ -140,6 +148,15 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$env:VP_DUMP_DIRS = '1'
& $vpPath | Add-Content -LiteralPath $dirsFile -Encoding UTF8
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Use Vite+'s generated environment so system-first shims stay last in PATH.
$configLine = Get-Content -LiteralPath $dirsFile | Where-Object { $_.StartsWith("config" + [char]9) } | Select-Object -Last 1
if ($configLine) {
$envFile = Join-Path $configLine.Substring(7) 'env.ps1'
if (Test-Path -LiteralPath $envFile -PathType Leaf) {
. $envFile
Add-Content -LiteralPath $dirsFile -Value ("path" + [char]9 + $env:PATH) -Encoding UTF8
}
}
`.trim();
return { command: "pwsh", args: ["-Command", script] };
}
Expand Down Expand Up @@ -177,6 +194,14 @@ vp_dir="\${SHIM_DIR:-\${INSTALL_DIR:-\${VP_HOME:-$HOME/.vite-plus}}/bin}"
if [ -x "$vp_dir/vp" ]; then
"$vp_dir/vp" --version > "$${VP_DIRS_FILE_ENV}"
VP_DUMP_DIRS=1 "$vp_dir/vp" >> "$${VP_DIRS_FILE_ENV}"
# Read the resolved config directory, including split/custom installations.
while IFS=$'\\t' read -r key value; do
if [ "$key" = config ] && [ -f "$value/env" ]; then
source "$value/env"
printf 'path\\t%s\\n' "$PATH" >> "$${VP_DIRS_FILE_ENV}"
break
fi
done < "$${VP_DIRS_FILE_ENV}"
fi
`.trim();
return { command: "bash", args: ["-c", script] };
Expand Down
2 changes: 1 addition & 1 deletion src/gitlab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function main(phase = "setup"): Promise<void> {
projectDir,
);

await installVitePlus(version, { env, prependPath: () => exportShellEnv("PATH", env.PATH) });
await installVitePlus(version, { env, exportPath: (value) => exportShellEnv("PATH", value) });

applyEnvironmentModes();
if (nodeVersion) run("vp", ["env", "use", nodeVersion], { cwd: projectDir });
Expand Down
19 changes: 18 additions & 1 deletion src/install-viteplus.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";
import { exec } from "@actions/exec";
import { addPath, warning } from "@actions/core";
import { addPath, warning, exportVariable } from "@actions/core";
import { writeFileSync } from "node:fs";
import { installVitePlus } from "./install-viteplus.js";
import type { Inputs } from "./types.js";
Expand All @@ -9,6 +9,7 @@ vi.mock("@actions/core", () => ({
info: vi.fn(),
warning: vi.fn(),
addPath: vi.fn(),
exportVariable: vi.fn(),
}));

vi.mock("@actions/exec", () => ({
Expand Down Expand Up @@ -86,6 +87,22 @@ describe("installVitePlus", () => {
expect(addPath).toHaveBeenCalledWith("/test/data/bin");
});

it("persists generated PATH while reserving GITHUB_PATH for the main bin", async () => {
const installedPath = "/custom/bin:/system/bin:/data/fallback-bin";
vi.stubEnv("PATH", "/custom/bin:/system/bin");
vi.mocked(exec).mockImplementationOnce(async (_command, _args, options) => {
writeDirsFile(options, "/custom/bin");
const env = (options as { env: Record<string, string> }).env;
writeFileSync(env.SETUP_VP_DIRS_FILE, `\npath\t${installedPath}\n`, { flag: "a" });
return 0;
});

await installVitePlus(baseInputs);

expect(exportVariable).toHaveBeenCalledWith("PATH", installedPath);
expect(addPath).toHaveBeenCalledExactlyOnceWith("/custom/bin");
});

it("should fall back to the legacy bin for Vite+ releases without VpDirs", async () => {
vi.stubEnv("HOME", "/home/runner");
vi.stubEnv("PATH", "/usr/bin");
Expand Down
8 changes: 6 additions & 2 deletions src/install-viteplus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { info, warning, addPath } from "@actions/core";
import { info, warning, addPath, exportVariable } from "@actions/core";
import { exec } from "@actions/exec";
import { delimiter, join } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
Expand All @@ -7,6 +7,7 @@ import {
createVitePlusDirsFile,
getInstallScriptCommand,
removeVitePlusDirsFile,
readVitePlusPath,
resolveVitePlusBinDir,
supportsVitePlusDirs,
VP_DIRS_FILE_ENV,
Expand Down Expand Up @@ -119,7 +120,10 @@ async function runInstallCommand(url: string, env: { [key: string]: string }): P

function ensureVitePlusBinInPath(version: string, dirsFile: string | undefined): void {
const binDir = resolveVitePlusBinDir(version, dirsFile, join(getVitePlusHome(), "bin"));
if (!process.env.PATH?.split(delimiter).includes(binDir)) {
const installedPath = readVitePlusPath(dirsFile);
// GITHUB_PATH only prepends; persist the full generated PATH through GITHUB_ENV.
if (installedPath !== undefined || !process.env.PATH?.split(delimiter).includes(binDir)) {
addPath(binDir);
}
if (installedPath !== undefined) exportVariable("PATH", installedPath);
}
Loading