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
30 changes: 14 additions & 16 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
workflow_dispatch:
inputs:
Expand Down Expand Up @@ -1137,12 +1136,6 @@ jobs:
- name: Build portable bundles
run: vp run build

# The full unit suite runs only on Ubuntu and skips PowerShell execution.
# Run these regressions in the OS matrix to exercise Bash on Linux/macOS
# and native powershell.exe on Windows; syntax checks cannot catch injection.
- name: Test Azure template ref validation and shell injection regressions
run: vp run test src/azure/template.test.ts

- name: Verify Azure runtime invalid phase smoke test
shell: bash
run: |
Expand All @@ -1162,9 +1155,14 @@ jobs:
[scriptblock]::Create((Get-Content -Raw azure/bootstrap.ps1)) > $null
[scriptblock]::Create((Get-Content -Raw gitlab/bootstrap.ps1)) > $null

test-portable-process:
name: Portable Windows command and cache regressions
runs-on: windows-latest
test-unit:
name: Unit tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
# Use the published CLI for repository checks, including manual runs.
env:
VP_PR_VERSION: ""
steps:
Expand All @@ -1173,13 +1171,16 @@ jobs:
uses: ./
with:
node-version-file: .node-version
- name: Test native Windows executables and cache links
run: vp run test src/ci/process.test.ts src/ci/cache-snapshot.test.ts
# Some tests inspect or execute dist/, so build on each runner first.
- name: Build
run: vp run build
- name: Unit tests
run: vp run test

build:
runs-on: ubuntu-latest
# This job verifies the action's own artifacts (dist/ committed and up to
# date, types, unit tests). Pin it to the latest published Vite+ even on a
# date, types, lint). Pin it to the latest published Vite+ even on a
# pr_version dispatch: a pre-release bundler could shift dist/ output and
# fail the "dist is up to date" diff for reasons unrelated to the change.
env:
Expand All @@ -1200,9 +1201,6 @@ jobs:
- name: Check
run: vp run check

- name: Unit tests
run: vp run test

- name: Build
run: vp run build

Expand Down
8 changes: 4 additions & 4 deletions src/auth.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 { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { configAuthentication, propagateProjectNpmrcAuth } from "./auth.js";
import { exportVariable, info } from "@actions/core";

Expand All @@ -21,7 +21,7 @@ vi.mock("node:fs", async () => {
});

describe("configAuthentication", () => {
const runnerTemp = "/tmp/runner";
const runnerTemp = resolve("/tmp/runner");

beforeEach(() => {
vi.stubEnv("RUNNER_TEMP", runnerTemp);
Expand Down Expand Up @@ -189,8 +189,8 @@ describe("configAuthentication", () => {
});

describe("propagateProjectNpmrcAuth", () => {
const runnerTemp = "/tmp/runner";
const projectDir = "/workspace/project";
const runnerTemp = resolve("/tmp/runner");
const projectDir = resolve("/workspace/project");
const npmrcPath = join(projectDir, ".npmrc");
const supplementalPath = join(runnerTemp, ".npmrc");

Expand Down
6 changes: 4 additions & 2 deletions src/azure/install-viteplus.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vite-plus/test";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { installVitePlus } from "./install-viteplus.js";

function writeDirsFile(env: Record<string, string>, bin: string): void {
Expand Down Expand Up @@ -122,8 +123,9 @@ describe("installVitePlus", () => {
logWarningFn: () => undefined,
});

expect(prependPath).toHaveBeenCalledWith("/home/runner/.vite-plus/bin");
expect(env.PATH).toBe("/home/runner/.vite-plus/bin:/usr/bin");
const bin = join("/home/runner", ".vite-plus", "bin");
expect(prependPath).toHaveBeenCalledWith(bin);
expect(env.PATH).toBe(`${bin}:/usr/bin`);
});

it.each([
Expand Down
3 changes: 2 additions & 1 deletion src/ci/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ describe("portable project auth", () => {
expect(result).toContain("//registry.example/npm/:_authToken=${NODE_AUTH_TOKEN}");
expect(result).not.toContain("secret");
expect(readFileSync(path.join(project, ".npmrc"), "utf8")).toBe(content);
expect(statSync(npmrc).mode & 0o777).toBe(0o600);
// Windows does not expose POSIX owner/group permission bits.
if (process.platform !== "win32") expect(statSync(npmrc).mode & 0o777).toBe(0o600);
expect(exporter).toHaveBeenCalledWith("NODE_AUTH_TOKEN", "secret");
expect(exporter).toHaveBeenCalledWith("CUSTOM_TOKEN", "other-secret");
expect(env.PNPM_CONFIG_USERCONFIG).toBe(npmrc);
Expand Down
3 changes: 2 additions & 1 deletion src/gitlab/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ describe("configureAuth", () => {
expect(targetEnv.NODE_AUTH_TOKEN).toBe("XXXXX-XXXXX-XXXXX-XXXXX");
expect(path.basename(path.dirname(npmrc))).toMatch(/^setup-vp-npmrc-/);
expect(npmrc).not.toBe(path.join(tmpdir(), `setup-vp-npmrc.${process.pid}`));
expect(statSync(npmrc).mode & 0o777).toBe(0o600);
// Windows does not expose POSIX owner/group permission bits.
if (process.platform !== "win32") expect(statSync(npmrc).mode & 0o777).toBe(0o600);
expect(readFileSync(npmrc, "utf8")).toBe(
"//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n@myorg:registry=https://npm.pkg.github.com/\n",
);
Expand Down
74 changes: 39 additions & 35 deletions src/gitlab/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,50 @@ describe("GitLab bootstrap", () => {
expect(result.status, result.stderr).toBe(0);
});

it.each([0, 23])("uses the image Node and preserves runtime exit code %i", (code) => {
const fixture = mkdtempSync(join(tmpdir(), "setup-vp-bootstrap-"));
const bin = join(fixture, "image bin");
mkdirSync(bin);
symlinkSync(process.execPath, join(bin, "node"));
writeFileSync(
join(bin, "curl"),
`#!/usr/bin/env bash
// This exercises the Unix bootstrap; Windows runners use bootstrap.ps1.
it.skipIf(process.platform === "win32").each([0, 23])(
"uses the image Node and preserves runtime exit code %i",
(code) => {
const fixture = mkdtempSync(join(tmpdir(), "setup-vp-bootstrap-"));
const bin = join(fixture, "image bin");
mkdirSync(bin);
symlinkSync(process.execPath, join(bin, "node"));
writeFileSync(
join(bin, "curl"),
`#!/usr/bin/env bash
case "$6" in
*/test-ref/dist/gitlab/index.mjs) cp "$SETUP_VP_TEST_DIR/runtime.mjs" "$8" ;;
*) exit 90 ;;
esac
`,
);
chmodSync(join(bin, "curl"), 0o755);
writeFileSync(
join(fixture, "runtime.mjs"),
`console.log("runtime started:", process.execPath); process.exit(${code});`,
);
try {
const result = spawnSync("bash", [bootstrapPath], {
encoding: "utf8",
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
TMPDIR: fixture,
SETUP_VP_TEST_DIR: fixture,
SETUP_VP_SETUP_REF: "test-ref",
SETUP_VP_NODE_MANAGER: "false",
},
});
expect(result.status, result.stderr).toBe(code);
expect(result.stdout).toContain(`runtime started: ${process.execPath}`);
expect(
readdirSync(fixture).filter((name) => name.startsWith("setup-vp-gitlab-runtime.")),
).toEqual([]);
} finally {
rmSync(fixture, { recursive: true, force: true });
}
});
);
chmodSync(join(bin, "curl"), 0o755);
writeFileSync(
join(fixture, "runtime.mjs"),
`console.log("runtime started:", process.execPath); process.exit(${code});`,
);
try {
const result = spawnSync("bash", [bootstrapPath], {
encoding: "utf8",
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
TMPDIR: fixture,
SETUP_VP_TEST_DIR: fixture,
SETUP_VP_SETUP_REF: "test-ref",
SETUP_VP_NODE_MANAGER: "false",
},
});
expect(result.status, result.stderr).toBe(code);
expect(result.stdout).toContain(`runtime started: ${process.execPath}`);
expect(
readdirSync(fixture).filter((name) => name.startsWith("setup-vp-gitlab-runtime.")),
).toEqual([]);
} finally {
rmSync(fixture, { recursive: true, force: true });
}
},
);

it("uses the shared runtime on Windows and checks native command failures", () => {
const bootstrap = readFileSync(new URL("../../gitlab/bootstrap.ps1", import.meta.url), "utf8");
Expand Down
93 changes: 39 additions & 54 deletions src/gitlab/install-sfw.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import type { IncomingMessage } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vite-plus/test";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { downloadFile, getSfwAssetName, setupSfw } from "./install-sfw.js";
import { commandPath } from "../ci/process.js";

vi.mock("node:child_process", () => ({ spawnSync: vi.fn() }));
vi.mock("../ci/process.js", () => ({ commandPath: vi.fn() }));

const tempDirs: string[] = [];

Expand All @@ -14,11 +19,8 @@ function tempDir(): string {
return dir;
}

function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}

afterEach(() => {
vi.resetAllMocks();
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
Expand All @@ -40,61 +42,44 @@ describe("GitLab sfw setup", () => {
});

it("uses an existing sfw command from PATH", async () => {
const dir = tempDir();
const binDir = path.join(dir, "bin");
mkdirSync(binDir);
const sfwBin = path.join(binDir, "sfw");
writeFileSync(sfwBin, "#!/usr/bin/env sh\nexit 0\n", "utf8");
chmodSync(sfwBin, 0o755);

const previousPath = process.env.PATH;
try {
process.env.PATH = `${binDir}:${previousPath || ""}`;
expect(await setupSfw([{}], { SETUP_VP_SFW: "true", PATH: process.env.PATH })).toBe("sfw");
} finally {
process.env.PATH = previousPath;
}
vi.mocked(commandPath).mockReturnValue(path.join(tempDir(), "sfw"));

expect(await setupSfw([{}], { SETUP_VP_SFW: "true" })).toBe("sfw");
expect(commandPath).toHaveBeenCalledWith("sfw");
expect(spawnSync).not.toHaveBeenCalled();
});

it("uses curl for default downloads", async () => {
const dir = tempDir();
const binDir = path.join(dir, "bin");
const outputPath = path.join(dir, "sfw");
const logFile = path.join(dir, "curl.log");
mkdirSync(binDir);

const curlBin = path.join(binDir, "curl");
writeFileSync(
curlBin,
const curl = path.join(dir, "bin with spaces", "curl");
vi.mocked(commandPath).mockReturnValue(curl);
vi.mocked(spawnSync).mockReturnValue({
pid: 1,
output: [],
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
status: 0,
signal: null,
});

await downloadFile("https://example.test/sfw", outputPath);

expect(commandPath).toHaveBeenCalledWith("curl");
expect(spawnSync).toHaveBeenCalledExactlyOnceWith(
curl,
[
"#!/usr/bin/env sh",
`printf '%s\\n' "$*" > ${shellQuote(logFile)}`,
'out=""',
'while [ "$#" -gt 0 ]; do',
' if [ "$1" = "-o" ]; then',
" shift",
' out="$1"',
" break",
" fi",
" shift",
"done",
'printf "sfw" > "$out"',
].join("\n"),
"utf8",
"-fsSL",
"--connect-timeout",
"5",
"--max-time",
"60",
"https://example.test/sfw",
"-o",
outputPath,
],
{ stdio: "ignore" },
);
chmodSync(curlBin, 0o755);

const previousPath = process.env.PATH;
try {
process.env.PATH = `${binDir}:${previousPath || ""}`;
await downloadFile("https://example.test/sfw", outputPath);
} finally {
process.env.PATH = previousPath;
}

expect(readFileSync(outputPath, "utf8")).toBe("sfw");
expect(readFileSync(logFile, "utf8")).toContain("https://example.test/sfw");
expect(readFileSync(logFile, "utf8")).toContain("--max-time 60");
});

it("times out stalled downloads", async () => {
Expand Down
Loading
Loading