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
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ Use the shared primitives under [`src/ci/`](src/ci/) for portable runtime behavi
- GitLab: edit the TypeScript runtime under [`src/gitlab/`](src/gitlab/). The template downloads and runs the `vp pack` bundle at `dist/gitlab/index.mjs`. See the [GitLab integration notes](rfcs/gitlab-integration.md) for design constraints and follow-up work.
- Azure Pipelines: edit the runtime under [`src/azure/`](src/azure/). Azure cannot execute the GitHub Action bundle; the template runs `dist/azure/index.mjs` in `prepare` and `finalize` phases around `Cache@2`. See the [Azure integration notes](rfcs/azure-pipelines-integration.md) for the design, parity table, and cache semantics.

### Installer Node Selection

All three integrations run the installer from the configured project directory. setup-vp selects the Node version: `node-version` takes precedence over `node-version-file`, followed by automatic project discovery. Discovery checks `.node-version`, `package.json#devEngines.runtime`, `package.json#engines.node`, and `.nvmrc` in each directory before checking its parent. It passes the selected value through `VP_SETUP_NODE_VERSION`. With no selection or with `node-manager: false`, it clears the override so the installer keeps its LTS default.

Installers that support this protocol resolve an explicit environment override once and reuse that Node version when it satisfies both the CLI and the pinned pnpm requirements. Otherwise, they use a separate LTS runtime for installation. The installer itself does not discover project pins or apply normal Node session or user defaults. For an override, it writes the exact requested version to the temporary path in `VP_SETUP_NODE_VERSION_FILE`; this remains the requested version even when installation needs a fallback. For an explicit input, setup-vp uses that result for `vp env use`. Automatic discovery retains its normal directory scope.

The temporary result is cleared before each installer attempt and removed after installation. Older installers do not write a result, so setup-vp retains the original selector and existing behavior. Updating setup-vp alone does not enable runtime reuse for an older Vite+ release.

## GitLab End-to-End Tests

Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. The [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) accepts `run-e2e` label requests for same-repository PRs and manual `workflow_dispatch` requests on `main`. The job uses the GitLab trigger secret without checking out or executing PR files on the GitHub runner.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ When `working-directory` is set, relative `run-install.cwd`, `node-version-file`

Omitting both `node-version` and `node-version-file` leaves the session without an override. With the Vite+ Node.js manager enabled, its shims search the current directory and its parents for `.node-version`, `package.json#devEngines.runtime`, `package.json#engines.node`, and `.nvmrc`, in that order. If the project does not declare a version, Vite+ uses the user-level default. Set this default with `vp env default <version>`. If no user-level default exists, Vite+ uses the latest LTS release.

For Vite+ releases that support an installer Node override, setup-vp also reads these project declarations before installation. It passes the selected version to the installer to avoid a second Node download when that version supports both the CLI and its installation dependencies. Without a selection, or when that version is incompatible, installation uses LTS. Older Vite+ installers retain their existing behavior.

`working-directory` applies to the action. Each later workflow step keeps its own working directory. Vite+ searches for Node.js version sources from each command's current working directory. For a subproject, set `working-directory` on the step that runs `node` or `vp`.

`node-manager: false` runs `vp env off node` (Vite+ 0.3.1+; `vp env off` on older versions), so `vp` commands prefer the Node.js already on `PATH`. It cannot be combined with `node-version` or `node-version-file`.
Expand Down
57 changes: 31 additions & 26 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

63 changes: 34 additions & 29 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

190 changes: 96 additions & 94 deletions dist/index.mjs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.7.0",
"semver": "^7.8.5",
"yaml": "^2.8.2",
"zod": "^4.0.0"
},
"devDependencies": {
"@e18e/eslint-plugin": "^0.8.1",
"@types/node": "^24.0.0",
"@types/semver": "^7.8.0",
"fallow": "3.33.1",
"typescript": "^7.0.0",
"vite": "catalog:",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions src/azure/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@ import { describe, expect, it, vi } from "vite-plus/test";
import { runFinalize, runPrepare } from "./index.js";

describe("Azure lifecycle", () => {
it("passes the Node selector to bootstrap and activates its exact result", async () => {
const run = vi.fn();
const installVitePlus = vi.fn(async () => "22.23.1");
await runPrepare(
{
SETUP_VP_VERSION: "1.3.0",
SETUP_VP_NODE_VERSION: "22",
SYSTEM_DEFAULTWORKINGDIRECTORY: process.cwd(),
},
{
installVitePlus,
prepareCacheMetadata: () => ({ ready: false }),
configureAuth: () => undefined,
setupSfw: async () => ({ executable: "vp", sfw: false }),
parseRunInstall: () => [],
runInstall: () => undefined,
getCommandOutput: () => "vp v1.3.0",
run,
parseInstalledVpVersion: () => "1.3.0",
prependPath: () => undefined,
setVariable: () => undefined,
logWarning: () => undefined,
logInfo: () => undefined,
},
);
expect(installVitePlus).toHaveBeenCalledWith(
"1.3.0",
expect.objectContaining({
projectDir: process.cwd(),
nodeVersion: "22",
}),
);
expect(run).toHaveBeenCalledWith("vp", ["env", "use", "22.23.1"], { cwd: process.cwd() });
});
it("runs prepare in install → cache order", async () => {
const calls: string[] = [];
await runPrepare(
Expand Down
25 changes: 11 additions & 14 deletions src/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,24 @@ export async function runPrepare(
warning: ports.logWarning,
};
const version = createVersionResolver(context).resolveVitePlusVersion(inputs, projectDir);
const nodeVersion =
inputs.nodeVersion ||
(inputs.nodeVersionFile
? createNodeVersionResolver(context).resolveNodeVersionFile(
inputs.nodeVersionFile,
projectDir,
)
: undefined);
let nodeVersion = createNodeVersionResolver(context).resolveNodeVersionInput(inputs, projectDir);

ports.setVariable("SETUP_VP_CACHE_HIT", "false");
ports.setVariable("SETUP_VP_CACHE_READY", "false");
ports.setVariable("SETUP_VP_SFW_READY", "false");
// Keep the setup runtime independent of the project's selected Node version.
ports.setVariable("SETUP_VP_BOOTSTRAP_NODE", process.execPath);

await ports.installVitePlus(version, {
env,
prependPath: (binDir) => ports.prependPath(binDir),
exportPath: (value) => ports.setVariable("PATH", value),
logWarningFn: ports.logWarning,
});
nodeVersion =
(await ports.installVitePlus(version, {
env,
projectDir,
nodeVersion,
nodeManager: inputs.nodeManager,
prependPath: (binDir) => ports.prependPath(binDir),
exportPath: (value) => ports.setVariable("PATH", value),
logWarningFn: ports.logWarning,
})) ?? nodeVersion;
// Finalize must use the resolved version, including pins from files or lockfiles.
ports.setVariable("SETUP_VP_RESOLVED_VERSION", version);

Expand Down
8 changes: 6 additions & 2 deletions src/azure/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ function fixture() {
}

describe("Azure parity", () => {
it("exports the full installer PATH for subsequent Azure tasks", async () => {
const { root, env, ports } = fixture();
it("exports the installer PATH and passes project Node discovery without a session override", async () => {
const { root, project, env, ports } = fixture();
writeFileSync(path.join(project, ".node-version"), "22.18.0");
const bin = path.join(root, "bin");
const data = path.join(root, "data");
const fallbackBin = path.join(data, "fallback-bin");
Expand All @@ -50,6 +51,8 @@ describe("Azure parity", () => {
installVitePlus(version, {
...options,
runInstall: (_url, installEnv) => {
expect(installEnv.VP_SETUP_NODE_VERSION).toBe("22.18.0");
writeFileSync(installEnv.VP_SETUP_NODE_VERSION_FILE!, "22.18.0");
writeFileSync(
installEnv.SETUP_VP_DIRS_FILE!,
`data\t${data}\nbin\t${bin}\ncache\t/cache\nconfig\t/config\nstate\t/state\n`,
Expand All @@ -62,6 +65,7 @@ describe("Azure parity", () => {
await runPrepare({ ...env, PATH: "/system/bin", SETUP_VP_VERSION: "latest" }, ports);

expect(ports.prependPath).toHaveBeenCalledWith(bin);
expect(ports.run).not.toHaveBeenCalledWith("vp", ["env", "use", "22.18.0"], expect.anything());
expect(ports.setVariable).toHaveBeenCalledWith(
"PATH",
[bin, "/system/bin", fallbackBin].join(path.delimiter),
Expand Down
14 changes: 14 additions & 0 deletions src/ci/install-viteplus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ beforeEach(() => {
});

describe("portable installer shell selection", () => {
it("passes Node selection and project cwd through the PowerShell fallback", async () => {
spawnSync.mockImplementation(
(command: string, _args: string[], spawnOptions: SpawnSyncOptions) => {
expect(spawnOptions.cwd).toBe("C:\\project");
expect(spawnOptions.env?.VP_SETUP_NODE_VERSION).toBe("22");
if (command === "pwsh") return result(null, missingCommand);
writeFileSync(spawnOptions.env!.VP_SETUP_NODE_VERSION_FILE!, "22.23.1");
return result(0);
},
);
expect(
await installVitePlus("0.2.9", { ...options, projectDir: "C:\\project", nodeVersion: "22" }),
).toBe("22.23.1");
});
it.each(["0.2.9", "0.3.1"])(
"uses Windows PowerShell when pwsh is missing for Vite+ %s",
async (version) => {
Expand Down
52 changes: 20 additions & 32 deletions src/ci/install-viteplus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { spawnSync } from "node:child_process";
import { getInstallScriptUrls, pkgPrNewCommitSha } from "./install-script-urls.js";
import { getInstallScriptUrls } from "./install-script-urls.js";
import { isWindows } from "./platform.js";
import { createInstallerNodeContext } from "./installer-node.js";
import { createInstallerEnvironment } from "./installer-env.js";
import {
appendFallbackBinToPath,
createVitePlusDirsFile,
getInstallScriptCommand,
removeVitePlusDirsFile,
resolveVitePlusBinDirs,
supportsVitePlusDirs,
VP_DIRS_FILE_ENV,
} from "./vp-dirs.js";

const INSTALL_MAX_ROUNDS = 2;
Expand All @@ -29,10 +28,12 @@ function runInstallCommand(
url: string,
env: Record<string, string>,
platform: NodeJS.Platform = process.platform,
cwd?: string,
): number {
const { command, args } = getInstallScriptCommand(url, platform, env.VP_VPDIRS_AWARE === "1");
const spawnOptions = {
env: { ...process.env, ...env },
cwd,
stdio: "inherit" as const,
};
let result = spawnSync(command, args, spawnOptions);
Expand All @@ -53,44 +54,29 @@ export async function installVitePlus(
options: {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
projectDir?: string;
nodeVersion?: string;
nodeManager?: boolean;
prependPath?: (binDir: string) => void;
exportPath?: (path: string) => void;
sleep?: (ms: number) => Promise<void>;
runInstall?: typeof runInstallCommand;
logWarningFn?: (message: string) => void;
} = {},
): Promise<void> {
): Promise<string | undefined> {
const platform = options.platform ?? process.platform;
const targetEnv = options.env ?? process.env;
const prependPath = options.prependPath;
const delay = options.sleep ?? sleep;
const runInstall = options.runInstall ?? runInstallCommand;
const warn = options.logWarningFn ?? console.warn;

const env: Record<string, string> = {
...Object.fromEntries(
Object.entries(targetEnv).filter(
(entry): entry is [string, string] => entry[1] !== undefined,
),
),
VP_VERSION: version,
VITE_PLUS_VERSION: version,
};

const detectDirs = supportsVitePlusDirs(version);
const dirsFile = detectDirs ? createVitePlusDirsFile() : undefined;
if (dirsFile) {
env.VP_VPDIRS_AWARE = "1";
env[VP_DIRS_FILE_ENV] = dirsFile;
} else {
delete env.VP_VPDIRS_AWARE;
delete env[VP_DIRS_FILE_ENV];
}

const prVersion = pkgPrNewCommitSha(version);
if (prVersion) {
env.VP_PR_VERSION = prVersion;
}
const nodeContext = createInstallerNodeContext(
options.nodeVersion,
options.nodeManager,
options.projectDir,
);
const { env, dirsFile } = createInstallerEnvironment(version, targetEnv, nodeContext.env);

// Prefer the install script pinned to the requested version's git ref. Fall
// back to the latest script only after all pinned sources fail (see
Expand All @@ -106,7 +92,8 @@ export async function installVitePlus(
for (const url of urls) {
attempt += 1;
try {
const exitCode = runInstall(url, env, platform);
nodeContext.reset();
const exitCode = runInstall(url, env, platform, options.projectDir);
if (exitCode === 0) return true;
failureReason = `exit code ${exitCode}`;
} catch (error) {
Expand Down Expand Up @@ -145,7 +132,7 @@ export async function installVitePlus(
if (pinned.length > 0) {
if (await tryUrls(pinned)) {
ensureBinInPath();
return;
return nodeContext.resolvedVersion();
}
warn(
`setup-vp: could not fetch the install script pinned to Vite+ ${version}. Falling back to the latest install script. The latest script may not be compatible with ${version}.`,
Expand All @@ -154,13 +141,14 @@ export async function installVitePlus(

if (await tryUrls(latest)) {
ensureBinInPath();
return;
return nodeContext.resolvedVersion();
}

throw new Error(
`Failed to install Vite+ after ${maxAttempts} attempts across ${totalUrls} URL(s): ${failureReason}`,
);
} finally {
if (dirsFile) removeVitePlusDirsFile(dirsFile);
nodeContext.dispose();
}
}
31 changes: 31 additions & 0 deletions src/ci/installer-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { pkgPrNewCommitSha } from "./install-script-urls.js";
import { createVitePlusDirsFile, supportsVitePlusDirs, VP_DIRS_FILE_ENV } from "./vp-dirs.js";

export function createInstallerEnvironment(
version: string,
parentEnv: NodeJS.ProcessEnv,
nodeEnv: Record<string, string>,
): { env: Record<string, string>; dirsFile: string | undefined } {
const env: Record<string, string> = {
...Object.fromEntries(
Object.entries(parentEnv).filter(
(entry): entry is [string, string] => entry[1] !== undefined,
),
),
...nodeEnv,
VP_VERSION: version,
// Keep compatibility with releases before the VP_* variable rename.
VITE_PLUS_VERSION: version,
};
const dirsFile = supportsVitePlusDirs(version) ? createVitePlusDirsFile() : undefined;
if (dirsFile) {
env.VP_VPDIRS_AWARE = "1";
env[VP_DIRS_FILE_ENV] = dirsFile;
} else {
delete env.VP_VPDIRS_AWARE;
delete env[VP_DIRS_FILE_ENV];
}
const prVersion = pkgPrNewCommitSha(version);
if (prVersion) env.VP_PR_VERSION = prVersion;
return { env, dirsFile };
}
Loading
Loading