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
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,47 @@ jobs:
- name: Verify installation
run: vp --version

test-preview-build:
# End-to-end coverage for the commit-pinned install path: resolve the
# newest pkg.pr.new preview build (0.0.0-commit.<sha>) from the registry
# bridge and install it through the version input, so the install script
# is fetched from that build's commit instead of main.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
# The version assertion below must see the bridge-resolved build. Pin
# VP_PR_VERSION to "" so a manual pr_version dispatch can't override it.
env:
VP_PR_VERSION: ""
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2

- name: Resolve the newest preview build from the registry bridge
id: preview
shell: bash
run: |
version="$(curl -fsSL --connect-timeout 5 --max-time 15 https://registry-bridge.viteplus.dev/vite-plus \
| jq -r '.time | to_entries | map(select(.key | test("^0\\.0\\.0-commit\\.[0-9a-f]{40}$"))) | max_by(.value).key')"
if [[ ! "$version" =~ ^0\.0\.0-commit\.[0-9a-f]{40}$ ]]; then
echo "Unexpected preview build version from the registry bridge: ${version}" >&2
exit 1
fi
echo "Resolved preview build: ${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"

- name: Setup Vite+
uses: ./
with:
version: ${{ steps.preview.outputs.version }}
run-install: false
cache: false

- name: Verify installation
shell: bash
run: vp --version | grep -F "${{ steps.preview.outputs.version }}"

test-node-version:
strategy:
fail-fast: false
Expand Down
2 changes: 1 addition & 1 deletion dist/azure/index.mjs

Large diffs are not rendered by default.

90 changes: 45 additions & 45 deletions dist/index.mjs

Large diffs are not rendered by default.

45 changes: 40 additions & 5 deletions gitlab/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,10 @@ setup_vp_install_viteplus_from() {
fi
}

setup_vp_install_viteplus() {
setup_vp_try_install_urls() {
setup_vp_round=1
while [ "$setup_vp_round" -le 2 ]; do
for setup_vp_url in \
"https://viteplus.dev/install.sh" \
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh"
do
for setup_vp_url in "$@"; do
echo "setup-vp: installing Vite+ ${SETUP_VP_VERSION} from ${setup_vp_url}"
if setup_vp_install_viteplus_from "$setup_vp_url"; then
return 0
Expand All @@ -72,6 +69,33 @@ setup_vp_install_viteplus() {
fi
done

return 1
}

# Prefer the install script pinned to the requested version's git ref (the
# release tag, or the commit itself for a pkg.pr.new preview build): the
# latest script tracks the latest CLI and can break installs of older
# versions. Fall back to the latest script only after all pinned sources fail.
# A missing tag or a full mirror outage then gives the previous behavior
# instead of blocking CI.
setup_vp_install_viteplus() {
if [ -n "$setup_vp_pinned_ref" ]; then
if setup_vp_try_install_urls \
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/${setup_vp_pinned_ref}/packages/cli/install.sh" \
"https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@${setup_vp_pinned_ref}/packages/cli/install.sh"
then
return 0
fi
echo "setup-vp: could not fetch the install script pinned to Vite+ ${SETUP_VP_VERSION}. Falling back to the latest install script. The latest script may not be compatible with ${SETUP_VP_VERSION}." >&2
fi

if setup_vp_try_install_urls \
"https://viteplus.dev/install.sh" \
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh"
then
return 0
fi

echo "setup-vp: failed to install Vite+ after retrying all installer URLs." >&2
return 1
}
Expand All @@ -97,6 +121,17 @@ setup_vp_pr_version=""
if [[ "$SETUP_VP_VERSION" =~ ^0\.0\.0-commit\.([0-9a-fA-F]{40})$ ]]; then
setup_vp_pr_version="${BASH_REMATCH[1]}"
fi

# Git ref that serves the install script for the requested version: the
# preview build's commit, or the `v<version>` release tag for an exact
# version. Dist-tags like "latest" do not map to a ref and keep the latest
# script.
setup_vp_pinned_ref=""
if [ -n "$setup_vp_pr_version" ]; then
setup_vp_pinned_ref="$(printf "%s" "$setup_vp_pr_version" | tr '[:upper:]' '[:lower:]')"
elif [[ "$SETUP_VP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
setup_vp_pinned_ref="v${SETUP_VP_VERSION}"
fi
setup_vp_install_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-install.XXXXXX")"
setup_vp_runtime_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-gitlab-runtime.XXXXXX.mjs")"
trap 'rm -f "$setup_vp_install_tmp" "$setup_vp_runtime_tmp"' EXIT
Expand Down
23 changes: 23 additions & 0 deletions src/azure/install-viteplus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ describe("installVitePlus", () => {
expect(runInstall.mock.calls[1]?.[0]).toContain("install.sh");
});

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

await expect(
installVitePlus("0.2.9", {
platform: "linux",
env: { PATH: "" },
prependPath: () => undefined,
sleep: async () => undefined,
runInstall,
logWarningFn: (message) => warnings.push(message),
}),
).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");
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);
});

it("routes pkg.pr.new commit builds through VP_PR_VERSION", async () => {
const runInstall = vi.fn(() => 0);
const sha = "a".repeat(40);
Expand Down
87 changes: 49 additions & 38 deletions src/azure/install-viteplus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,20 @@ 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 "../ci/install-script-urls.js";
import { logWarning } from "./commands.js";

const INSTALL_URLS_SH = [
"https://viteplus.dev/install.sh",
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh",
];
const INSTALL_URLS_PS1 = [
"https://viteplus.dev/install.ps1",
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.ps1",
];
const INSTALL_MAX_ROUNDS = 2;
const INSTALL_RETRY_DELAY_MS = 2000;
const CURL_TIMEOUT_FLAGS = "--connect-timeout 5 --max-time 15";
const PWSH_TIMEOUT_SEC = 15;
const PKG_PR_NEW_COMMIT_RE = /^0\.0\.0-commit\.([0-9a-f]{40})$/i;

export function getVitePlusHome(platform: NodeJS.Platform = process.platform): string {
const home =
platform === "win32" ? process.env.USERPROFILE || homedir() : process.env.HOME || homedir();
return join(home, ".vite-plus");
}

function pkgPrNewCommitSha(version: string): string | undefined {
return version.match(PKG_PR_NEW_COMMIT_RE)?.[1];
}

function runInstallCommand(
url: string,
env: Record<string, string>,
Expand Down Expand Up @@ -93,40 +81,63 @@ export async function installVitePlus(
env.VP_NODE_MANAGER = options.nodeManager ? "yes" : "no";
}

const urls = platform === "win32" ? INSTALL_URLS_PS1 : INSTALL_URLS_SH;
const maxAttempts = INSTALL_MAX_ROUNDS * urls.length;
// 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
// ../ci/install-script-urls.ts for the rationale).
const { pinned, latest } = getInstallScriptUrls(version, platform);
const totalUrls = pinned.length + latest.length;
const maxAttempts = INSTALL_MAX_ROUNDS * totalUrls;
let failureReason = "";
let attempt = 0;

for (let round = 0; round < INSTALL_MAX_ROUNDS; round += 1) {
for (const url of urls) {
attempt += 1;
try {
const exitCode = runInstall(url, env, platform);
if (exitCode === 0) {
const binDir = join(getVitePlusHome(platform), "bin");
if (!targetEnv.PATH?.includes(binDir)) {
const separator = platform === "win32" ? ";" : ":";
targetEnv.PATH = `${binDir}${separator}${targetEnv.PATH || ""}`;
prependPath?.(binDir);
}
return;
const tryUrls = async (urls: string[]): Promise<boolean> => {
for (let round = 0; round < INSTALL_MAX_ROUNDS; round += 1) {
for (const url of urls) {
attempt += 1;
try {
const exitCode = runInstall(url, env, platform);
if (exitCode === 0) return true;
failureReason = `exit code ${exitCode}`;
} catch (error) {
failureReason = error instanceof Error ? error.message : String(error);
}
failureReason = `exit code ${exitCode}`;
} catch (error) {
failureReason = error instanceof Error ? error.message : String(error);
}

if (attempt < maxAttempts) {
warn(
`setup-vp: failed to install Vite+ from ${url} (${failureReason}). Retrying in ${INSTALL_RETRY_DELAY_MS}ms... (attempt ${attempt + 1}/${maxAttempts})`,
);
await delay(INSTALL_RETRY_DELAY_MS);
if (attempt < maxAttempts) {
warn(
`setup-vp: failed to install Vite+ from ${url} (${failureReason}). Retrying in ${INSTALL_RETRY_DELAY_MS}ms... (attempt ${attempt + 1}/${maxAttempts})`,
);
await delay(INSTALL_RETRY_DELAY_MS);
}
}
}
return false;
};

const ensureBinInPath = (): void => {
const binDir = join(getVitePlusHome(platform), "bin");
if (!targetEnv.PATH?.includes(binDir)) {
const separator = platform === "win32" ? ";" : ":";
targetEnv.PATH = `${binDir}${separator}${targetEnv.PATH || ""}`;
prependPath?.(binDir);
}
};

if (pinned.length > 0) {
if (await tryUrls(pinned)) {
ensureBinInPath();
return;
}
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}.`,
);
}

if (await tryUrls(latest)) {
ensureBinInPath();
return;
}

throw new Error(
`Failed to install Vite+ after ${maxAttempts} attempts across ${urls.length} URL(s): ${failureReason}`,
`Failed to install Vite+ after ${maxAttempts} attempts across ${totalUrls} URL(s): ${failureReason}`,
);
}
58 changes: 58 additions & 0 deletions src/ci/install-script-urls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect } from "vite-plus/test";
import { getInstallScriptUrls, pkgPrNewCommitSha } from "./install-script-urls.js";

const commitSha = "7d848b3da1987fa60b4cf18487fcc36a2a697e94";

describe("pkgPrNewCommitSha", () => {
it("extracts the SHA from a pkg.pr.new commit build", () => {
expect(pkgPrNewCommitSha(`0.0.0-commit.${commitSha}`)).toBe(commitSha);
});

it("returns undefined for regular versions and near-miss SHA lengths", () => {
expect(pkgPrNewCommitSha("0.2.9")).toBeUndefined();
expect(pkgPrNewCommitSha(`0.0.0-commit.${commitSha.slice(0, 39)}`)).toBeUndefined();
});
});

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",
]);
});

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("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", "^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("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