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
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ The PR's `rebuild-bundle.yml` must match the default branch before the publisher

Renovate opens a PR when SocketDev publishes a new `sfw-free` release. The custom manager in [`.github/renovate.json`](.github/renovate.json) updates the shared `SFW_VERSION` pin in `src/ci/install-sfw.ts` for all three integrations.

### Installer Checksum Pins

`src/ci/installer-checksums.json` is the trust source for installer downloads in
all three integrations. It records immutable upstream commits and SHA-256
checksums. Historical pins keep older installer directory layouts available.
The default pin also serves dist-tags, unknown versions, and preview builds.

To add a pin, review `packages/cli/install.sh` and `packages/cli/install.ps1` at
the intended commit in `voidzero-dev/vite-plus`. Review any scripts they execute,
including `install-legacy.sh` and `install-legacy.ps1`. Then run:

```bash
node scripts/update-installer-checksums.mjs <version> <reviewed-commit-sha>
```

Add `--default` to select the new default installer. The command fetches the
scripts without executing them and records their checksums, including the
legacy helpers when present. It does not run during installation or builds.
Review the manifest diff and rebuild all three bundles. Do not add a runtime
checksum lookup or an unverified download fallback. If upstream adds another
script dependency, extend verification before approving that installer.

## Releasing

Publish releases as Git tags, not as an npm package. Keep `package.json.version` aligned with the release tag. Consumers pin an exact tag such as `voidzero-dev/setup-vp@v1.21.1` or a commit SHA. Do not move the `v1` major tag, which is frozen at `v1.15.0`.
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,20 @@ Separate package-manager modes require Vite+ 0.3.1+. Any explicit configuration,
| `version` | The installed version of Vite+ |
| `cache-hit` | Boolean indicating if cache was restored |

## Installer integrity

GitHub Actions, GitLab, and Azure download installer scripts from immutable
upstream commits and check their SHA-256 checksums before execution. The expected
checksums ship with setup-vp. Both download mirrors use the same checksums, and
the installer's legacy helper is also verified before execution.

Known versions use their matching installer. Dist-tags, preview builds, and
versions without a bundled checksum use the checksum-pinned default installer.
If all sources for a known version fail, setup-vp also tries that default.
The requested CLI version is unchanged. Update setup-vp when a newer CLI requires
an installer that its current checksum pins do not support. These checksums cover
installer scripts; the installer still downloads the requested CLI packages.

## Caching

### Reuse an installed version
Expand Down
64 changes: 45 additions & 19 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

62 changes: 44 additions & 18 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

62 changes: 44 additions & 18 deletions dist/index.mjs

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions scripts/update-installer-checksums.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";

// Supply the reviewed upstream commit explicitly. Never resolve a moving ref
// while installing Vite+: these checksums are part of the setup-vp release.
const [version, commit, flag] = process.argv.slice(2);
if (
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version ?? "") ||
!/^[0-9a-f]{40}$/.test(commit ?? "") ||
(flag !== undefined && flag !== "--default")
) {
throw new Error(
"Usage: node scripts/update-installer-checksums.mjs <version> <reviewed-commit-sha> [--default]",
);
}

const manifestPath = new URL("../src/ci/installer-checksums.json", import.meta.url);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const checksums = {};
for (const extension of ["sh", "ps1"]) {
const url = `https://raw.githubusercontent.com/voidzero-dev/vite-plus/${commit}/packages/cli/install.${extension}`;
const response = await fetch(url, { signal: AbortSignal.timeout(15_000), redirect: "error" });
if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);
const bytes = Buffer.from(await response.arrayBuffer());
if (bytes.length === 0) throw new Error(`Empty installer: ${url}`);
checksums[extension] = createHash("sha256").update(bytes).digest("hex");
if (bytes.includes(`install-legacy.${extension}`)) {
const legacyUrl = url.replace(`install.${extension}`, `install-legacy.${extension}`);
const legacy = await fetch(legacyUrl, {
signal: AbortSignal.timeout(15_000),
redirect: "error",
});
if (!legacy.ok) throw new Error(`Download failed (${legacy.status}): ${legacyUrl}`);
const legacyBytes = Buffer.from(await legacy.arrayBuffer());
if (legacyBytes.length === 0) throw new Error(`Empty installer: ${legacyUrl}`);
checksums[`legacy_${extension}`] = createHash("sha256").update(legacyBytes).digest("hex");
}
}
manifest.releases[version] = { commit, ...checksums };
if (flag === "--default") manifest.defaultVersion = version;
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
35 changes: 21 additions & 14 deletions src/azure/install-viteplus.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vite-plus/test";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { getInstallScriptSources, type InstallScriptSource } from "../ci/install-script-urls.js";
import { installVitePlus } from "./install-viteplus.js";

function writeDirsFile(env: Record<string, string>, bin: string): void {
Expand All @@ -20,7 +21,11 @@ describe("installVitePlus", () => {
it("uses PowerShell installers on Windows and bash installers on Unix", async () => {
const calls: NodeJS.Platform[] = [];
const runInstall = vi.fn(
(_url: string, env: Record<string, string>, platform: NodeJS.Platform = process.platform) => {
(
_source: InstallScriptSource,
env: Record<string, string>,
platform: NodeJS.Platform = process.platform,
) => {
calls.push(platform);
writeDirsFile(env, `/test/${platform}/bin`);
return 0;
Expand Down Expand Up @@ -48,13 +53,13 @@ describe("installVitePlus", () => {
for (const [, installEnv] of runInstall.mock.calls) {
expect(installEnv.VP_NODE_MANAGER).toBeUndefined();
}
expect(runInstall.mock.calls[0]?.[0]).toContain("install.ps1");
expect(runInstall.mock.calls[1]?.[0]).toContain("install.sh");
expect(runInstall.mock.calls[0]?.[0].url).toContain("install.ps1");
expect(runInstall.mock.calls[1]?.[0].url).toContain("install.sh");
});

it("installs exact versions with the release-tag script before the latest script", async () => {
it("tries the release-specific checksum pin before the default pin", async () => {
// Fail every attempt so the full URL order is observable.
const runInstall = vi.fn((_url: string, _env: Record<string, string>) => 1);
const runInstall = vi.fn((_source: InstallScriptSource, _env: Record<string, string>) => 1);
const warnings: string[] = [];

await expect(
Expand All @@ -72,18 +77,20 @@ describe("installVitePlus", () => {
}),
).rejects.toThrow(/after 8 attempts across 4 URL\(s\)/);

const urls = runInstall.mock.calls.map((call) => call[0]);
expect(urls[0]).toContain("/v0.2.9/packages/cli/install.sh");
const urls = runInstall.mock.calls.map((call) => call[0].url);
expect(urls[0]).toBe(getInstallScriptSources("0.2.9", "linux").pinned[0]!.url);
expect(urls[1]).toContain("jsdelivr");
expect(urls[4]).toBe("https://viteplus.dev/install.sh");
expect(warnings.some((message) => message.includes("Falling back to the latest"))).toBe(true);
expect(urls[4]).toBe(getInstallScriptSources("latest", "linux").fallback[0]!.url);
expect(
warnings.some((message) => message.includes("Falling back to the checksum-pinned default")),
).toBe(true);
const installEnv = runInstall.mock.calls[0]?.[1] as Record<string, string>;
expect(installEnv.VP_VPDIRS_AWARE).toBeUndefined();
expect(installEnv.SETUP_VP_DIRS_FILE).toBeUndefined();
});

it("routes pkg.pr.new commit builds through VP_PR_VERSION", async () => {
const runInstall = vi.fn((_url: string, env: Record<string, string>) => {
const runInstall = vi.fn((_source: InstallScriptSource, env: Record<string, string>) => {
writeDirsFile(env, "/test/data/bin");
return 0;
});
Expand All @@ -99,7 +106,7 @@ describe("installVitePlus", () => {
});

const installCalls = runInstall.mock.calls as unknown as Array<
[string, Record<string, string>, NodeJS.Platform?]
[InstallScriptSource, Record<string, string>, NodeJS.Platform?]
>;
expect(installCalls[0]?.[1]?.VP_PR_VERSION).toBe(sha);
expect(installCalls[0]?.[1]?.VP_VPDIRS_AWARE).toBe("1");
Expand All @@ -109,7 +116,7 @@ describe("installVitePlus", () => {
it("uses the installed version to resolve the latest dist-tag", async () => {
const prependPath = vi.fn();
const env = { HOME: "/home/runner", PATH: "/usr/bin" };
const runInstall = vi.fn((_url: string, installEnv: Record<string, string>) => {
const runInstall = vi.fn((_source: InstallScriptSource, installEnv: Record<string, string>) => {
writeFileSync(installEnv.SETUP_VP_DIRS_FILE, "vp v0.2.9\n");
return 0;
});
Expand All @@ -134,7 +141,7 @@ describe("installVitePlus", () => {
{ version: "latest", output: "vp v0.3.0\n" },
])("fails when $version does not report valid VpDirs", async ({ version, output }) => {
const prependPath = vi.fn();
const runInstall = vi.fn((_url: string, env: Record<string, string>) => {
const runInstall = vi.fn((_source: InstallScriptSource, env: Record<string, string>) => {
if (output !== undefined) writeFileSync(env.SETUP_VP_DIRS_FILE, output);
return 0;
});
Expand All @@ -159,7 +166,7 @@ describe("installVitePlus", () => {
it("prepends the bin directory reported by the installed payload", async () => {
const prependPath = vi.fn();
const env = { PATH: "/usr/bin" };
const runInstall = vi.fn((_url: string, installEnv: Record<string, string>) => {
const runInstall = vi.fn((_source: InstallScriptSource, installEnv: Record<string, string>) => {
writeDirsFile(installEnv, "/test/data/bin");
return 0;
});
Expand Down
77 changes: 44 additions & 33 deletions src/ci/install-script-urls.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vite-plus/test";
import { getInstallScriptUrls, pkgPrNewCommitSha } from "./install-script-urls.js";
import { getInstallScriptSources, pkgPrNewCommitSha } from "./install-script-urls.js";

const commitSha = "7d848b3da1987fa60b4cf18487fcc36a2a697e94";

Expand All @@ -14,45 +14,56 @@ describe("pkgPrNewCommitSha", () => {
});
});

describe("getInstallScriptUrls", () => {
it("pins an exact version to its release tag, with jsDelivr as mirror", () => {
const { pinned, latest } = getInstallScriptUrls("0.2.9", "linux");
expect(pinned).toEqual([
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/v0.2.9/packages/cli/install.sh",
"https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@v0.2.9/packages/cli/install.sh",
]);
expect(latest).toEqual([
"https://viteplus.dev/install.sh",
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh",
describe("getInstallScriptSources", () => {
it("pins historical versions to a commit and the same checksum on both mirrors", () => {
const { pinned } = getInstallScriptSources("0.2.9", "linux");
expect(pinned.map(({ url }) => url)).toEqual([
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/73bdd105c2b7c263b55f7e9b48a37a8933cc317a/packages/cli/install.sh",
"https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@73bdd105c2b7c263b55f7e9b48a37a8933cc317a/packages/cli/install.sh",
]);
expect(pinned[0]!.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(pinned[1]!.sha256).toBe(pinned[0]!.sha256);
expect(pinned[0]!.legacy).toBeUndefined();
});

it("pins prerelease versions to their release tag", () => {
const { pinned } = getInstallScriptUrls("0.1.21-alpha.7", "linux");
expect(pinned[0]).toContain("/v0.1.21-alpha.7/");
it("keeps prerelease-specific pins", () => {
const { pinned } = getInstallScriptSources("0.1.21-alpha.7", "linux");
expect(pinned[0]!.url).toContain("/c61621ac610ad114746b98d7e457ca4e13171837/");
});

it("pins pkg.pr.new commit builds to the commit, lowercasing the SHA", () => {
const { pinned } = getInstallScriptUrls(`0.0.0-commit.${commitSha.toUpperCase()}`, "linux");
expect(pinned).toEqual([
`https://raw.githubusercontent.com/voidzero-dev/vite-plus/${commitSha}/packages/cli/install.sh`,
`https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@${commitSha}/packages/cli/install.sh`,
]);
it.each([
"latest",
"next",
"99.0.0",
"^0.2.0",
"0.2",
"0.2.x",
"0.2.9+build.5",
"",
"__proto__",
`0.0.0-commit.${commitSha}`,
])("uses only the bundled fallback pin for %j", (version) => {
const { pinned, fallback } = getInstallScriptSources(version, "linux");
expect(pinned).toEqual([]);
expect(fallback).toEqual(getInstallScriptSources("latest", "linux").fallback);
expect(fallback).toHaveLength(2);
});

it.each(["latest", "next", "^0.2.0", "0.2", "0.2.x", "0.2.9+build.5", ""])(
"does not pin %j",
(version) => {
const { pinned, latest } = getInstallScriptUrls(version, "linux");
expect(pinned).toEqual([]);
expect(latest).toHaveLength(2);
it.each(["linux", "win32"] as const)(
"pins all %s files, including legacy dependencies",
(platform) => {
const { fallback } = getInstallScriptSources("latest", platform);
const extension = platform === "win32" ? "ps1" : "sh";
for (const source of fallback) {
expect(source.url).toMatch(
new RegExp(`[/@][a-f0-9]{40}/packages/cli/install\\.${extension}$`),
);
expect(source.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(source.legacy?.url).toBe(
source.url.replace(`install.${extension}`, `install-legacy.${extension}`),
);
expect(source.legacy?.sha256).toMatch(/^[a-f0-9]{64}$/);
}
},
);

it("uses install.ps1 on Windows for both pinned and latest URLs", () => {
const { pinned, latest } = getInstallScriptUrls("0.2.9", "win32");
for (const url of [...pinned, ...latest]) {
expect(url).toMatch(/install\.ps1$/);
}
});
});
Loading
Loading