Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
112ee5f
feat: add sfw input to wrap vp install with Socket Firewall Free
fengmk2 May 26, 2026
fafe07a
test(ci): add test-sfw-blocks-malicious job using lodahs canary
fengmk2 May 26, 2026
c4df421
ci: limit test-sfw to ubuntu-latest, document sfw rustls TLS limitation
fengmk2 May 26, 2026
f5f2b16
feat(sfw): fall back to plain vp install on non-Linux with a warning
fengmk2 May 26, 2026
b3e3a58
chore: point sfw non-Linux warning at setup-vp tracker issue
fengmk2 May 26, 2026
4d71ec2
docs: point README sfw fallback note at setup-vp#73 tracker
fengmk2 May 26, 2026
b0bece4
docs(ci): collapse sfw-free issue references to setup-vp#73 tracker
fengmk2 May 26, 2026
f0618a4
docs: point isSfwSupported comment at setup-vp#73 tracker
fengmk2 May 26, 2026
d2ed525
test(ci): add test-sfw-package-managers covering pnpm/npm/yarn/bun
fengmk2 May 26, 2026
f13d0ba
test(ci): fix yarn job + promote bun to required
fengmk2 May 26, 2026
9fbb519
test(ci): force Yarn nodeLinker=node-modules so verify step is uniform
fengmk2 May 26, 2026
e6024b3
test(ci): merge test-sfw and test-sfw-package-managers into one matrix
fengmk2 May 26, 2026
aa92f44
ci+docs: address code-review findings on the matrix-merge diff
fengmk2 May 26, 2026
381ef2b
ci+src: handle PR #72 review comments
fengmk2 May 26, 2026
2e0144f
ci: only test latest vp release in test-sfw matrix
fengmk2 May 26, 2026
65e179d
ci: also drop alpha from test-sfw-alpine and test-sfw-blocks-malicious
fengmk2 May 26, 2026
b6a100f
ci: drop single-value version axis from sfw jobs entirely
fengmk2 May 26, 2026
c69ca6f
ci+src: fix code-review findings on commits since aa92f44
fengmk2 May 26, 2026
91936e2
feat(sfw): auto-detect pre-installed sfw + pin version + Renovate rule
fengmk2 May 27, 2026
18c1cc4
fix(sfw): hard-gate macOS/Windows before PATH detect + add negative a…
fengmk2 May 27, 2026
fbb345b
feat(sfw): cache the sfw binary via @actions/cache
fengmk2 May 27, 2026
77a3bfa
ci: add one-off verify-vp-1686-sfw workflow_dispatch
fengmk2 May 28, 2026
3f209a2
ci: switch verify-vp-1686-sfw to push-with-paths-filter trigger
fengmk2 May 28, 2026
80dd299
ci(verify): drop socketdev/action, install sfw manually from same URL…
fengmk2 May 28, 2026
b323a21
ci(verify): also exercise musl via alpine:3.23 container
fengmk2 May 28, 2026
43f7df0
ci(verify): bump pkg-pr-new target to vp PR #1703 (v0.1.23 release)
fengmk2 May 28, 2026
d34bd47
feat(sfw): bump SFW_VERSION to v1.11.0
fengmk2 May 28, 2026
2161235
test(sfw): add branch coverage for setupSfw, installSfw, isSfwSupported
fengmk2 May 28, 2026
d9921a6
feat(sfw): support macOS and Windows (vite-plus v0.1.23+)
fengmk2 May 29, 2026
a75aa97
refactor(sfw): drop cacheHit dead state; prove blocking on macOS/Windows
fengmk2 May 29, 2026
113b54c
chore(deps): vite-plus ^0.1.23; assert sfw blocks on composition path
fengmk2 May 29, 2026
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
131 changes: 131 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,137 @@ jobs:
- name: Verify vp exec works
run: vp exec node -e "console.log('vp exec works in Alpine')"

test-sfw:
# On Linux: sfw wraps vp install end-to-end (sfw binary downloaded,
# `sfw vp install` runs).
# On macOS / Windows: sfw is temporarily unsupported because sfw v1.10.0
# issues a TLS cert with an empty EKU extension that vp's rustls rejects
# (UnknownIssuer). The action emits a warning and falls back to plain
# `vp install`; no sfw binary is downloaded. We assert that fallback by
# checking `sfw` is NOT on PATH while the install still succeeds.
# Tracking: https://github.com/voidzero-dev/setup-vp/issues/73
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, alpha]
runs-on: ${{ matrix.os }}
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project with a real dependency
shell: bash
run: |
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json

- name: Setup Vite+ (${{ matrix.version }}) with sfw
uses: ./
with:
version: ${{ matrix.version }}
sfw: true
run-install: |
- cwd: test-project
cache: false

- name: Verify sfw is on PATH (Linux only)
if: runner.os == 'Linux'
run: sfw --version

- name: Verify sfw fallback on non-Linux (sfw NOT installed)
if: runner.os != 'Linux'
shell: bash
run: |
if command -v sfw >/dev/null 2>&1; then
echo "ERROR: expected sfw to be absent on ${{ runner.os }} (fallback path), but it is on PATH"
exit 1
fi
echo "OK: sfw is not on PATH; fallback to plain vp install confirmed"

- name: Verify dependency installed
working-directory: test-project
run: vp exec node -e "console.log(require('is-odd')(3))"

test-sfw-alpine:
strategy:
fail-fast: false
matrix:
version: [latest, alpha]
runs-on: ubuntu-latest
container:
image: alpine:3.23
steps:
- name: Install Alpine dependencies
run: apk add --no-cache bash curl gcompat libstdc++

- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project with a real dependency
run: |
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json

- name: Setup Vite+ (${{ matrix.version }}) with sfw (musl)
uses: ./
with:
version: ${{ matrix.version }}
sfw: true
run-install: |
- cwd: test-project
cache: false

- name: Verify sfw is on PATH (musl)
run: sfw --version

- name: Verify dependency installed under sfw (musl)
working-directory: test-project
run: vp exec node -e "console.log(require('is-odd')(3))"

test-sfw-blocks-malicious:
# Verifies sfw actually intercepts a known-malicious package, not just
# that it wraps the install. Uses `lodahs` (lodash typosquat), the same
# canary SocketDev's own workflows use:
# https://github.com/SocketDev/bun-security-scanner/blob/main/.github/workflows/test.yml
# If this job ever stops blocking, either sfw is misconfigured or the
# canary itself has been delisted — swap it for another Socket-flagged
# package from https://socket.dev/blog/category/threat-research.
strategy:
fail-fast: false
matrix:
version: [latest, alpha]
runs-on: ubuntu-latest
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Create test project with a benign dependency
shell: bash
run: |
mkdir -p test-project
cd test-project
echo '{"name":"test-project","private":true,"dependencies":{"is-odd":"^3.0.1"}}' > package.json

- name: Setup Vite+ (${{ matrix.version }}) with sfw and install benign dep
uses: ./
with:
version: ${{ matrix.version }}
sfw: true
run-install: |
- cwd: test-project
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
cache: false

- name: Assert sfw blocks malicious package (lodahs typosquat of lodash)
shell: bash
working-directory: test-project
run: |
if sfw vp install lodahs; then
echo "ERROR: sfw failed to block lodahs (lodash typosquat)"
exit 1
else
echo "SUCCESS: sfw blocked lodahs as expected"
fi

Comment thread
fengmk2 marked this conversation as resolved.
build:
runs-on: ubuntu-latest
steps:
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ GitHub Action to set up [Vite+](https://viteplus.dev) (`vp`) with dependency cac
- Optionally set up a specific Node.js version via `vp env use`
- Cache project dependencies with auto-detection of lock files
- Optionally run `vp install` after setup
- Optionally wrap `vp install` with [Socket Firewall Free (`sfw`)](https://docs.socket.dev/docs/socket-firewall-free) to block malicious dependencies
- Support for all major package managers (npm, pnpm, yarn, bun)

## Usage
Expand Down Expand Up @@ -135,6 +136,24 @@ steps:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

### With Socket Firewall Free (sfw)

Set `sfw: true` to wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free). The action downloads the matching `sfw` binary from the upstream [releases](https://github.com/SocketDev/sfw-free/releases) (auto-detected per OS/arch, with musl support on Alpine) and runs `sfw vp install …` so the underlying npm / pnpm / yarn fetches are inspected before packages are installed:

```yaml
steps:
- uses: actions/checkout@v6
- uses: voidzero-dev/setup-vp@v1
with:
sfw: true
run-install: true
```

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

> [!IMPORTANT]
> **Linux-only for now.** `sfw` ships a self-signed CA whose certificate has an empty Extended Key Usage extension. Strict TLS stacks like rustls (used by `vp`) reject it as `UnknownIssuer`, so `vp install` fails the TLS handshake on macOS / Windows. To keep `sfw: true` safe to set unconditionally in cross-platform workflows, the action **falls back to plain `vp install` with a warning on non-Linux platforms** — it does not download the `sfw` binary there. The platform check will be relaxed once the upstream work tracked in [voidzero-dev/setup-vp#73](https://github.com/voidzero-dev/setup-vp/issues/73) lands.

### Alpine Container

Alpine Linux uses musl libc instead of glibc. Install compatibility packages before using the action:
Expand Down Expand Up @@ -178,6 +197,7 @@ jobs:
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
| `working-directory` | Project directory used for relative paths, lockfile auto-detection, environment checks, and default install | No | Workspace root |
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
| `sfw` | Wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) (`sfw`) | No | `false` |
| `cache` | Enable caching of project dependencies | No | `false` |
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
| `registry-url` | Optional registry to set up for auth. Sets the registry in `.npmrc` and reads auth from `NODE_AUTH_TOKEN` | No | |
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ inputs:
description: "Run `vp install` after setup. Accepts boolean or YAML object with cwd/args."
required: false
default: "true"
sfw:
description: "Wrap `vp install` with Socket Firewall Free (sfw) to block malicious dependency fetches. Downloads the sfw binary from https://github.com/SocketDev/sfw-free/releases. See https://docs.socket.dev/docs/socket-firewall-free."
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
required: false
default: "false"
node-version:
description: "Node.js version to install via `vp env use`. Defaults to Node.js latest LTS version."
required: false
Expand Down
124 changes: 62 additions & 62 deletions dist/index.mjs

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { saveState, getState, setFailed, info, setOutput, warning } from "@actio
import { exec, getExecOutput } from "@actions/exec";
import { getInputs } from "./inputs.js";
import { installVitePlus } from "./install-viteplus.js";
import { installSfw, isSfwSupported } from "./install-sfw.js";
import { runViteInstall } from "./run-install.js";
import { restoreCache } from "./cache-restore.js";
import { saveCache } from "./cache-save.js";
Expand Down Expand Up @@ -43,9 +44,23 @@ async function runMain(inputs: Inputs): Promise<void> {
await restoreCache(inputs);
}

// Step 6: Run vp install if requested
// Step 6: Install Socket Firewall Free if requested (must run before vp install).
// On non-Linux platforms, sfw is temporarily unsupported (see isSfwSupported)
// and we fall back to plain `vp install` with a warning.
let effectiveSfw = inputs.sfw;
if (inputs.sfw && !isSfwSupported()) {
warning(
`sfw is temporarily only supported on Linux (process.platform=${process.platform}); falling back to plain \`vp install\`. Track upstream: https://github.com/voidzero-dev/setup-vp/issues/73`,
);
effectiveSfw = false;
}
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
if (effectiveSfw && inputs.runInstall.length > 0) {
await installSfw();
}
Comment thread
fengmk2 marked this conversation as resolved.
Outdated

// Step 7: Run vp install if requested
if (inputs.runInstall.length > 0) {
await runViteInstall(inputs);
await runViteInstall({ ...inputs, sfw: effectiveSfw });
}

// Print version info at the end
Expand Down
13 changes: 13 additions & 0 deletions src/inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("getInputs", () => {
nodeVersionFile: undefined,
workingDirectory: undefined,
runInstall: [],
sfw: false,
cache: false,
cacheDependencyPath: undefined,
});
Expand Down Expand Up @@ -106,6 +107,18 @@ describe("getInputs", () => {
expect(inputs.cache).toBe(true);
});

it("should parse sfw input", () => {
vi.mocked(getInput).mockReturnValue("");
vi.mocked(getBooleanInput).mockImplementation((name) => {
if (name === "sfw") return true;
return false;
});

const inputs = getInputs();

expect(inputs.sfw).toBe(true);
});

it("should parse node-version-file input", () => {
vi.mocked(getInput).mockImplementation((name) => {
if (name === "node-version-file") return ".nvmrc";
Expand Down
1 change: 1 addition & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function getInputs(): Inputs {
nodeVersionFile: getInput("node-version-file") || undefined,
workingDirectory: getInput("working-directory") || undefined,
runInstall: parseRunInstall(getInput("run-install")),
sfw: getBooleanInput("sfw"),
cache: getBooleanInput("cache"),
cacheDependencyPath: getInput("cache-dependency-path") || undefined,
registryUrl: getInput("registry-url") || undefined,
Expand Down
66 changes: 66 additions & 0 deletions src/install-sfw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect } from "vite-plus/test";
import { getSfwAssetName, isSfwSupported } from "./install-sfw.js";

describe("getSfwAssetName", () => {
it("returns macOS arm64 asset", () => {
expect(getSfwAssetName("darwin", "arm64", false)).toBe("sfw-free-macos-arm64");
});

it("returns macOS x64 asset", () => {
expect(getSfwAssetName("darwin", "x64", false)).toBe("sfw-free-macos-x86_64");
});

it("ignores isMusl on darwin", () => {
expect(getSfwAssetName("darwin", "arm64", true)).toBe("sfw-free-macos-arm64");
expect(getSfwAssetName("darwin", "x64", true)).toBe("sfw-free-macos-x86_64");
});

it("returns Linux glibc arm64 asset", () => {
expect(getSfwAssetName("linux", "arm64", false)).toBe("sfw-free-linux-arm64");
});

it("returns Linux glibc x64 asset", () => {
expect(getSfwAssetName("linux", "x64", false)).toBe("sfw-free-linux-x86_64");
});

it("returns Linux musl arm64 asset", () => {
expect(getSfwAssetName("linux", "arm64", true)).toBe("sfw-free-musl-linux-arm64");
});

it("returns Linux musl x64 asset", () => {
expect(getSfwAssetName("linux", "x64", true)).toBe("sfw-free-musl-linux-x86_64");
});

it("returns Windows arm64 asset", () => {
expect(getSfwAssetName("win32", "arm64", false)).toBe("sfw-free-windows-arm64.exe");
});

it("returns Windows x64 asset", () => {
expect(getSfwAssetName("win32", "x64", false)).toBe("sfw-free-windows-x86_64.exe");
});

it("ignores isMusl on win32", () => {
expect(getSfwAssetName("win32", "x64", true)).toBe("sfw-free-windows-x86_64.exe");
});

it("throws on unsupported platform", () => {
expect(() => getSfwAssetName("freebsd" as NodeJS.Platform, "x64", false)).toThrow(
/freebsd\/x64/,
);
});

it("throws on unsupported arch", () => {
expect(() => getSfwAssetName("linux", "ia32", false)).toThrow(/linux\/ia32/);
});

it("includes libc in error message for unsupported Linux arch", () => {
expect(() => getSfwAssetName("linux", "ia32", true)).toThrow(/musl/);
expect(() => getSfwAssetName("linux", "ia32", false)).toThrow(/glibc/);
});
});

describe("isSfwSupported", () => {
it("returns true on Linux, false elsewhere (matches current platform)", () => {
expect(isSfwSupported()).toBe(process.platform === "linux");
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
});
});
Loading
Loading