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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ steps:

`sfw` is only applied when `run-install` is enabled; other `vp` commands (e.g. `vp env use`, `vp --version`) run unwrapped.

For Vite+ preview builds (`0.0.0-commit.<40-character SHA>`), setup-vp automatically disables `sfw`, even when it is enabled, and logs a warning. Dependency installation runs with plain `vp install`. This applies to GitHub Actions, GitLab CI/CD, and Azure Pipelines, including when `sfw` is already on `PATH`.

The action pins the `sfw` version it downloads so a re-run of the same commit gets the same binary.

#### Advanced: stricter supply chain via `socketdev/action`
Expand Down
8 changes: 4 additions & 4 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.mjs

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion src/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createVersionResolver } from "../ci/version-file.js";
import { createNodeVersionResolver } from "../ci/node-version-file.js";
import { resolutionContext } from "../ci/resolution.js";
import { nodeManagerOffArgs } from "../ci/node-manager.js";
import { pkgPrNewCommitSha } from "../ci/install-script-urls.js";
import { configureAuth, isReservedAuthVariable } from "../ci/auth.js";
import { analyzeProjectNpmrc, readNpmrc } from "../ci/npmrc.js";
import { prepareCacheMetadata } from "../ci/cache.js";
Expand Down Expand Up @@ -92,6 +93,8 @@ export async function runPrepare(
prependPath: (binDir) => ports.prependPath(binDir),
logWarningFn: ports.logWarning,
});
// Finalize must use the resolved version, including pins from files or lockfiles.
ports.setVariable("SETUP_VP_RESOLVED_VERSION", version);

const versionOutput =
inputs.nodeManager === false || inputs.packageManager !== undefined
Expand All @@ -113,7 +116,11 @@ export async function runPrepare(
const runtimePath = path.resolve(process.argv[1] || "");
ports.setVariable("SETUP_VP_RUNTIME_PATH", runtimePath);

if (inputs.sfw && ports.parseRunInstall(inputs.runInstall).length > 0) {
if (
inputs.sfw &&
!pkgPrNewCommitSha(version) &&
ports.parseRunInstall(inputs.runInstall).length > 0
) {
try {
const asset = getSfwAssetName(process.platform, process.arch, isMuslLinux());
const sfwCache = path.join(env.PIPELINE_WORKSPACE || inputs.workspaceRoot, ".setup-vp-sfw");
Expand Down Expand Up @@ -183,6 +190,8 @@ export async function runFinalize(
const installCommand = await ports.setupSfw(runInstallEntries, {
env,
sfwEnabled: inputs.sfw,
vitePlusVersion: env.SETUP_VP_RESOLVED_VERSION || inputs.version,
logWarning: ports.logWarning,
exportVariable: (name, value) => {
if (value === undefined) return;
if (name === "PATH") ports.prependPath(value.split(path.delimiter)[0]!);
Expand Down
46 changes: 45 additions & 1 deletion src/azure/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { configureAuth } from "../ci/auth.js";
import { setupSfw } from "../ci/install-sfw.js";
import { parseRunInstall } from "../ci/run-install.js";
import { runPrepare, runFinalize } from "./index.js";
import type { AzurePorts } from "./index.js";
Expand All @@ -21,7 +22,7 @@ function fixture() {
installVitePlus: vi.fn(async () => {}),
prepareCacheMetadata: vi.fn(() => ({ ready: false })),
configureAuth: vi.fn(configureAuth),
setupSfw: vi.fn(async () => "vp" as const),
setupSfw: vi.fn<typeof setupSfw>().mockResolvedValue("vp"),
parseRunInstall,
runInstall: vi.fn(),
getCommandOutput: vi.fn((): string | undefined => "vp v0.3.1"),
Expand Down Expand Up @@ -100,6 +101,49 @@ describe("Azure parity", () => {
);
});

it.each(["explicit", "package.json", "version-file"])(
"disables sfw and its cache for a preview resolved from %s",
async (source) => {
const { project, env, ports } = fixture();
const version = "0.0.0-commit.7d848b3da1987fa60b4cf18487fcc36a2a697e94";
const target: NodeJS.ProcessEnv = { ...env, SETUP_VP_SFW: "true" };
if (source === "explicit") {
target.SETUP_VP_VERSION = version;
} else if (source === "version-file") {
writeFileSync(
path.join(project, "pnpm-workspace.yaml"),
`catalog:\n vite-plus: ${version}\n`,
);
target.SETUP_VP_VERSION_FILE = "pnpm-workspace.yaml";
} else {
writeFileSync(
path.join(project, "package.json"),
JSON.stringify({ devDependencies: { "vite-plus": version } }),
);
}
// Azure carries task.setvariable values into the following task's environment.
ports.setVariable.mockImplementation((name, value) => {
target[name] = value;
});
ports.setupSfw.mockImplementation(setupSfw);

await runPrepare(target, ports);

expect(ports.installVitePlus).toHaveBeenCalledWith(version, expect.any(Object));
expect(target.SETUP_VP_SFW_READY).toBe("false");
expect(target.SETUP_VP_SFW_CACHE_DIR).toBeUndefined();
expect(target.SETUP_VP_RESOLVED_VERSION).toBe(version);
// Finalize has the original sfw input but does not receive version-file from the template.
delete target.SETUP_VP_VERSION_FILE;
await runFinalize(target, ports);

expect(ports.runInstall).toHaveBeenCalledWith([{}], project, "vp", target);
expect(ports.logWarning).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining(`automatically disabled for Vite+ preview build ${version}`),
);
},
);

it.each(["$(CUSTOM_TOKEN)", "$(MISSING_SECRET)"])(
"removes an unresolved custom auth macro %s before install",
async (value) => {
Expand Down
55 changes: 54 additions & 1 deletion src/ci/install-sfw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,66 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { setupSfw, SFW_VERSION } from "./install-sfw.js";
import { commandPath } from "./process.js";

vi.mock("./process.js", () => ({ commandPath: () => undefined }));
vi.mock("./process.js", () => ({ commandPath: vi.fn() }));
const directories: string[] = [];
afterEach(() => {
vi.resetAllMocks();
for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true });
});

describe("portable sfw preview handling", () => {
const vitePlusVersion = "0.0.0-commit.7d848b3da1987fa60b4cf18487fcc36a2a697e94";

it.each(["linux", "darwin", "win32"] as const)(
"skips sfw lookup, downloads, and exports for previews on %s",
async (platform) => {
vi.mocked(commandPath).mockReturnValue("/bin/sfw");
const download = vi.fn();
const exportVariable = vi.fn();
const logWarning = vi.fn();
const env = { SETUP_VP_SFW: "true", PATH: "/bin" };

const installCommand = await setupSfw([{}], {
env,
vitePlusVersion,
platform,
download,
exportVariable,
logWarning,
});

expect(installCommand).toBe("vp");
expect(logWarning).toHaveBeenCalledExactlyOnceWith(
`sfw was requested but is automatically disabled for Vite+ preview build ${vitePlusVersion}; Socket Firewall Free will not be used.`,
);
expect(commandPath).not.toHaveBeenCalled();
expect(download).not.toHaveBeenCalled();
expect(exportVariable).not.toHaveBeenCalled();
expect(env.PATH).toBe("/bin");
},
);

it("does not warn when sfw is already disabled for a preview", async () => {
const logWarning = vi.fn();
expect(await setupSfw([{}], { sfwEnabled: false, vitePlusVersion, logWarning })).toBe("vp");
expect(logWarning).not.toHaveBeenCalled();
});

it.each(["0.3.2", "0.3.3-alpha.1", "latest", "next"])(
"keeps sfw enabled for %s",
async (version) => {
vi.mocked(commandPath).mockReturnValue("/bin/sfw");
const logWarning = vi.fn();
expect(await setupSfw([{}], { sfwEnabled: true, vitePlusVersion: version, logWarning })).toBe(
"sfw",
);
expect(logWarning).not.toHaveBeenCalled();
},
);
});

describe("portable sfw cache", () => {
it("reuses a complete versioned asset without a second download", async () => {
const cacheDirectory = mkdtempSync(path.join(tmpdir(), "setup-vp-sfw-cache-"));
Expand Down
12 changes: 10 additions & 2 deletions src/ci/install-sfw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { get as httpGet } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { commandPath } from "./process.js";
import type { ExportVariable, InstallCommand, RunInstallEntry } from "./types.js";
import { resolveSfwEnabled } from "./sfw.js";
import type { ExportVariable, InstallCommand, LogFn, RunInstallEntry } from "./types.js";

export const SFW_VERSION = "v1.15.1";
const SFW_RELEASE_BASE = `https://github.com/SocketDev/sfw-free/releases/download/${SFW_VERSION}`;
Expand Down Expand Up @@ -152,6 +153,8 @@ export async function setupSfw(
options: {
env?: NodeJS.ProcessEnv;
sfwEnabled?: boolean;
vitePlusVersion?: string;
logWarning?: LogFn;
exportVariable?: ExportVariable;
platform?: NodeJS.Platform;
arch?: string;
Expand All @@ -167,7 +170,12 @@ export async function setupSfw(
const isMusl = options.isMusl ?? isMuslLinux();
const download = options.download ?? downloadFile;

if (!sfwEnabled) return "vp";
const effectiveSfw = resolveSfwEnabled(
sfwEnabled,
options.vitePlusVersion ?? "",
options.logWarning ?? console.warn,
);
if (!effectiveSfw) return "vp";

if (runInstallEntries.length === 0) {
console.log(
Expand Down
12 changes: 12 additions & 0 deletions src/ci/sfw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { pkgPrNewCommitSha } from "./install-script-urls.js";
import type { LogFn } from "./types.js";

export function resolveSfwEnabled(enabled: boolean, version: string, logWarning: LogFn): boolean {
if (!enabled) return false;
if (!pkgPrNewCommitSha(version)) return true;

logWarning(
`sfw was requested but is automatically disabled for Vite+ preview build ${version}; Socket Firewall Free will not be used.`,
);
return false;
}
39 changes: 37 additions & 2 deletions src/gitlab/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { tmpdir } from "node:os";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { installVitePlus } from "../ci/install-viteplus.js";
import { setupSfw } from "../ci/install-sfw.js";
import { run } from "../ci/process.js";
import { getCommandOutput } from "../ci/process.js";
import { commandPath, getCommandOutput, run, runWithOutput } from "../ci/process.js";
import { applyEnvironmentModes, isEntrypoint, main } from "./index.js";

vi.mock("../ci/process.js", () => ({
Expand All @@ -25,6 +24,8 @@ const directories: string[] = [];
afterEach(() => {
vi.unstubAllEnvs();
vi.clearAllMocks();
vi.mocked(commandPath).mockReset();
vi.restoreAllMocks();
for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true });
});

Expand Down Expand Up @@ -95,6 +96,40 @@ describe("GitLab setup parity", () => {
expect(run).toHaveBeenCalledWith("vp", ["env", "use", "22"], expect.any(Object));
});

it.each(["explicit", "package.json"])(
"disables sfw for a preview resolved from %s and warns once",
async (source) => {
const root = fixture();
const version = "0.0.0-commit.7d848b3da1987fa60b4cf18487fcc36a2a697e94";
if (source === "explicit") {
vi.stubEnv("SETUP_VP_VERSION", version);
} else {
writeFileSync(
path.join(root, "app/package.json"),
JSON.stringify({ devDependencies: { "vite-plus": version } }),
);
}
vi.stubEnv("SETUP_VP_SFW", "true");
vi.stubEnv("SETUP_VP_RUN_INSTALL", "true");
vi.mocked(commandPath).mockReturnValue("/bin/sfw");
vi.mocked(getCommandOutput).mockReturnValue(`vp v${version}`);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});

await main();

expect(installVitePlus).toHaveBeenCalledWith(version, expect.any(Object));
expect(commandPath).not.toHaveBeenCalled();
expect(runWithOutput).toHaveBeenCalledExactlyOnceWith(
"vp",
["install"],
expect.objectContaining({ cwd: path.join(root, "app") }),
);
expect(warn).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining(`automatically disabled for Vite+ preview build ${version}`),
);
},
);

it("rejects Node opt-out conflicts before installation", async () => {
fixture();
vi.stubEnv("SETUP_VP_NODE_MANAGER", "false");
Expand Down
2 changes: 1 addition & 1 deletion src/gitlab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function main(phase = "setup"): Promise<void> {
copyFileSync(process.argv[1]!, path.join(workspaceRoot, ".setup-vp-runtime.mjs"));
}

const installCommand = await setupSfw(runInstallEntries);
const installCommand = await setupSfw(runInstallEntries, env, version);
await runInstall(runInstallEntries, projectDir, installCommand);
if (cacheSaveEnabled) cache.save();

Expand Down
2 changes: 2 additions & 0 deletions src/gitlab/install-sfw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ export { downloadFile, getSfwAssetName, isMuslLinux, SFW_VERSION };
export async function setupSfw(
runInstallEntries: RunInstallEntry[],
env: NodeJS.ProcessEnv = process.env,
vitePlusVersion = "",
): Promise<InstallCommand> {
return setupSfwCore(runInstallEntries, {
env,
vitePlusVersion,
exportVariable: (name, value) => exportShellEnv(name, value, env),
});
}
35 changes: 34 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ vi.mock("@actions/core", async (importOriginal) => {
...actual,
getState: vi.fn(() => "true"),
info: vi.fn(),
saveState: vi.fn(),
setOutput: vi.fn(),
};
});
vi.mock("./inputs.js", () => ({
Expand All @@ -20,10 +22,26 @@ vi.mock("./inputs.js", () => ({
vi.mock("./cache-save.js", () => ({
saveCache: vi.fn(),
}));
vi.mock("@actions/exec", () => ({
exec: vi.fn(),
getExecOutput: vi.fn(async () => ({ stdout: "vp v0.3.2" })),
}));
vi.mock("./install-viteplus.js", () => ({ installVitePlus: vi.fn() }));
vi.mock("./install-sfw.js", () => ({ setupSfw: vi.fn() }));
vi.mock("./run-install.js", () => ({ runViteInstall: vi.fn() }));
vi.mock("./auth.js", () => ({
configAuthentication: vi.fn(),
propagateProjectNpmrcAuth: vi.fn(),
}));
vi.mock("./version-file.js", () => ({ resolveVitePlusVersion: vi.fn() }));

import { info } from "@actions/core";
import { saveCache } from "./cache-save.js";
import { runPost } from "./index.js";
import { runMain, runPost } from "./index.js";
import { installVitePlus } from "./install-viteplus.js";
import { setupSfw } from "./install-sfw.js";
import { runViteInstall } from "./run-install.js";
import { resolveVitePlusVersion } from "./version-file.js";
import type { Inputs } from "./types.js";

const mockedInfo = vi.mocked(info);
Expand All @@ -37,6 +55,21 @@ const inputs = (cache: boolean, cacheSave: boolean): Inputs => ({
cacheSave,
});

describe("runMain sfw", () => {
it("passes the resolved preview version to sfw setup and installs without sfw", async () => {
const version = "0.0.0-commit.7d848b3da1987fa60b4cf18487fcc36a2a697e94";
vi.mocked(resolveVitePlusVersion).mockReturnValue(version);
vi.mocked(setupSfw).mockResolvedValue(false);
const requested = { ...inputs(false, true), sfw: true, runInstall: [{}] };

await runMain(requested);

expect(installVitePlus).toHaveBeenCalledWith({ ...requested, version });
expect(setupSfw).toHaveBeenCalledWith({ ...requested, version });
expect(runViteInstall).toHaveBeenCalledWith({ ...requested, sfw: false });
});
});

describe("runPost", () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down
8 changes: 3 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { resolveVitePlusVersion } from "./version-file.js";
import { configAuthentication, propagateProjectNpmrcAuth } from "./auth.js";
import { getConfiguredProjectDir, parseInstalledVpVersion } from "./utils.js";

async function runMain(inputs: Inputs): Promise<void> {
export async function runMain(inputs: Inputs): Promise<void> {
// Mark that post action should run
saveState(State.IsPost, "true");
const projectDir = getConfiguredProjectDir(inputs);
Expand Down Expand Up @@ -64,10 +64,8 @@ async function runMain(inputs: Inputs): Promise<void> {
}

// Step 6: Install Socket Firewall Free if requested (must run before vp install).
// setupSfw centralizes all the decision branches: run-install disabled, sfw
// already on PATH (e.g. via socketdev/action@<sha>), supported platform
// (downloads our pinned binary), unsupported platform (falls back).
const effectiveSfw = await setupSfw(inputs);
// Use the resolved version so preview pins in files also disable sfw.
const effectiveSfw = await setupSfw({ ...inputs, version });

// Step 7: Run vp install if requested
if (inputs.runInstall.length > 0) {
Expand Down
Loading
Loading