Skip to content

Commit 06b723b

Browse files
authored
feat: pin the install script to the requested version (#127)
The action always installed vp with the latest install.sh, so an install-script change like the XDG directory layout in voidzero-dev/vite-plus#2346 could break installing older versions. The script is now fetched from the git ref matching the requested version: - Exact versions (`0.2.9`, `0.1.21-alpha.7`): the `v<version>` release tag on raw.githubusercontent.com, with jsDelivr as an independent mirror (both verified to serve `packages/cli/install.sh` back to v0.1.0). - pkg.pr.new preview builds (`0.0.0-commit.<sha>`): the script from that exact commit, so a preview build always installs with the script it was built with. - Dist-tags (`latest`, `next`): unchanged, the latest script matches whatever they resolve to. If all pinned sources fail (missing tag, mirror outage), the install warns and falls back to the latest script, so availability degrades to the previous behavior instead of blocking CI. Worst case for pinned versions is 8 attempts across 4 URLs instead of 4 across 2. The URL selection is shared in `src/ci/install-script-urls.ts` and applied to the GitHub action, the Azure runtime, and the GitLab bootstrap.
1 parent f388e2f commit 06b723b

10 files changed

Lines changed: 444 additions & 137 deletions

File tree

.github/workflows/test.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,47 @@ jobs:
4141
- name: Verify installation
4242
run: vp --version
4343

44+
test-preview-build:
45+
# End-to-end coverage for the commit-pinned install path: resolve the
46+
# newest pkg.pr.new preview build (0.0.0-commit.<sha>) from the registry
47+
# bridge and install it through the version input, so the install script
48+
# is fetched from that build's commit instead of main.
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
os: [ubuntu-latest, windows-latest]
53+
runs-on: ${{ matrix.os }}
54+
# The version assertion below must see the bridge-resolved build. Pin
55+
# VP_PR_VERSION to "" so a manual pr_version dispatch can't override it.
56+
env:
57+
VP_PR_VERSION: ""
58+
steps:
59+
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
60+
61+
- name: Resolve the newest preview build from the registry bridge
62+
id: preview
63+
shell: bash
64+
run: |
65+
version="$(curl -fsSL --connect-timeout 5 --max-time 15 https://registry-bridge.viteplus.dev/vite-plus \
66+
| jq -r '.time | to_entries | map(select(.key | test("^0\\.0\\.0-commit\\.[0-9a-f]{40}$"))) | max_by(.value).key')"
67+
if [[ ! "$version" =~ ^0\.0\.0-commit\.[0-9a-f]{40}$ ]]; then
68+
echo "Unexpected preview build version from the registry bridge: ${version}" >&2
69+
exit 1
70+
fi
71+
echo "Resolved preview build: ${version}"
72+
echo "version=${version}" >> "$GITHUB_OUTPUT"
73+
74+
- name: Setup Vite+
75+
uses: ./
76+
with:
77+
version: ${{ steps.preview.outputs.version }}
78+
run-install: false
79+
cache: false
80+
81+
- name: Verify installation
82+
shell: bash
83+
run: vp --version | grep -F "${{ steps.preview.outputs.version }}"
84+
4485
test-node-version:
4586
strategy:
4687
fail-fast: false

dist/azure/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/index.mjs

Lines changed: 45 additions & 45 deletions
Large diffs are not rendered by default.

gitlab/bootstrap.sh

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,10 @@ setup_vp_install_viteplus_from() {
5353
fi
5454
}
5555

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

72+
return 1
73+
}
74+
75+
# Prefer the install script pinned to the requested version's git ref (the
76+
# release tag, or the commit itself for a pkg.pr.new preview build): the
77+
# latest script tracks the latest CLI and can break installs of older
78+
# versions. Fall back to the latest script only after all pinned sources fail.
79+
# A missing tag or a full mirror outage then gives the previous behavior
80+
# instead of blocking CI.
81+
setup_vp_install_viteplus() {
82+
if [ -n "$setup_vp_pinned_ref" ]; then
83+
if setup_vp_try_install_urls \
84+
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/${setup_vp_pinned_ref}/packages/cli/install.sh" \
85+
"https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@${setup_vp_pinned_ref}/packages/cli/install.sh"
86+
then
87+
return 0
88+
fi
89+
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
90+
fi
91+
92+
if setup_vp_try_install_urls \
93+
"https://viteplus.dev/install.sh" \
94+
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh"
95+
then
96+
return 0
97+
fi
98+
7599
echo "setup-vp: failed to install Vite+ after retrying all installer URLs." >&2
76100
return 1
77101
}
@@ -97,6 +121,17 @@ setup_vp_pr_version=""
97121
if [[ "$SETUP_VP_VERSION" =~ ^0\.0\.0-commit\.([0-9a-fA-F]{40})$ ]]; then
98122
setup_vp_pr_version="${BASH_REMATCH[1]}"
99123
fi
124+
125+
# Git ref that serves the install script for the requested version: the
126+
# preview build's commit, or the `v<version>` release tag for an exact
127+
# version. Dist-tags like "latest" do not map to a ref and keep the latest
128+
# script.
129+
setup_vp_pinned_ref=""
130+
if [ -n "$setup_vp_pr_version" ]; then
131+
setup_vp_pinned_ref="$(printf "%s" "$setup_vp_pr_version" | tr '[:upper:]' '[:lower:]')"
132+
elif [[ "$SETUP_VP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
133+
setup_vp_pinned_ref="v${SETUP_VP_VERSION}"
134+
fi
100135
setup_vp_install_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-install.XXXXXX")"
101136
setup_vp_runtime_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-gitlab-runtime.XXXXXX.mjs")"
102137
trap 'rm -f "$setup_vp_install_tmp" "$setup_vp_runtime_tmp"' EXIT

src/azure/install-viteplus.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,29 @@ describe("installVitePlus", () => {
3737
expect(runInstall.mock.calls[1]?.[0]).toContain("install.sh");
3838
});
3939

40+
it("installs exact versions with the release-tag script before the latest script", async () => {
41+
// Fail every attempt so the full URL order is observable.
42+
const runInstall = vi.fn((_url: string) => 1);
43+
const warnings: string[] = [];
44+
45+
await expect(
46+
installVitePlus("0.2.9", {
47+
platform: "linux",
48+
env: { PATH: "" },
49+
prependPath: () => undefined,
50+
sleep: async () => undefined,
51+
runInstall,
52+
logWarningFn: (message) => warnings.push(message),
53+
}),
54+
).rejects.toThrow(/after 8 attempts across 4 URL\(s\)/);
55+
56+
const urls = runInstall.mock.calls.map((call) => call[0]);
57+
expect(urls[0]).toContain("/v0.2.9/packages/cli/install.sh");
58+
expect(urls[1]).toContain("jsdelivr");
59+
expect(urls[4]).toBe("https://viteplus.dev/install.sh");
60+
expect(warnings.some((message) => message.includes("Falling back to the latest"))).toBe(true);
61+
});
62+
4063
it("routes pkg.pr.new commit builds through VP_PR_VERSION", async () => {
4164
const runInstall = vi.fn(() => 0);
4265
const sha = "a".repeat(40);

src/azure/install-viteplus.ts

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,20 @@ import { homedir } from "node:os";
22
import { join } from "node:path";
33
import { setTimeout as sleep } from "node:timers/promises";
44
import { spawnSync } from "node:child_process";
5+
import { getInstallScriptUrls, pkgPrNewCommitSha } from "../ci/install-script-urls.js";
56
import { logWarning } from "./commands.js";
67

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

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

27-
function pkgPrNewCommitSha(version: string): string | undefined {
28-
return version.match(PKG_PR_NEW_COMMIT_RE)?.[1];
29-
}
30-
3119
function runInstallCommand(
3220
url: string,
3321
env: Record<string, string>,
@@ -93,40 +81,63 @@ export async function installVitePlus(
9381
env.VP_NODE_MANAGER = options.nodeManager ? "yes" : "no";
9482
}
9583

96-
const urls = platform === "win32" ? INSTALL_URLS_PS1 : INSTALL_URLS_SH;
97-
const maxAttempts = INSTALL_MAX_ROUNDS * urls.length;
84+
// Prefer the install script pinned to the requested version's git ref. Fall
85+
// back to the latest script only after all pinned sources fail (see
86+
// ../ci/install-script-urls.ts for the rationale).
87+
const { pinned, latest } = getInstallScriptUrls(version, platform);
88+
const totalUrls = pinned.length + latest.length;
89+
const maxAttempts = INSTALL_MAX_ROUNDS * totalUrls;
9890
let failureReason = "";
9991
let attempt = 0;
10092

101-
for (let round = 0; round < INSTALL_MAX_ROUNDS; round += 1) {
102-
for (const url of urls) {
103-
attempt += 1;
104-
try {
105-
const exitCode = runInstall(url, env, platform);
106-
if (exitCode === 0) {
107-
const binDir = join(getVitePlusHome(platform), "bin");
108-
if (!targetEnv.PATH?.includes(binDir)) {
109-
const separator = platform === "win32" ? ";" : ":";
110-
targetEnv.PATH = `${binDir}${separator}${targetEnv.PATH || ""}`;
111-
prependPath?.(binDir);
112-
}
113-
return;
93+
const tryUrls = async (urls: string[]): Promise<boolean> => {
94+
for (let round = 0; round < INSTALL_MAX_ROUNDS; round += 1) {
95+
for (const url of urls) {
96+
attempt += 1;
97+
try {
98+
const exitCode = runInstall(url, env, platform);
99+
if (exitCode === 0) return true;
100+
failureReason = `exit code ${exitCode}`;
101+
} catch (error) {
102+
failureReason = error instanceof Error ? error.message : String(error);
114103
}
115-
failureReason = `exit code ${exitCode}`;
116-
} catch (error) {
117-
failureReason = error instanceof Error ? error.message : String(error);
118-
}
119104

120-
if (attempt < maxAttempts) {
121-
warn(
122-
`setup-vp: failed to install Vite+ from ${url} (${failureReason}). Retrying in ${INSTALL_RETRY_DELAY_MS}ms... (attempt ${attempt + 1}/${maxAttempts})`,
123-
);
124-
await delay(INSTALL_RETRY_DELAY_MS);
105+
if (attempt < maxAttempts) {
106+
warn(
107+
`setup-vp: failed to install Vite+ from ${url} (${failureReason}). Retrying in ${INSTALL_RETRY_DELAY_MS}ms... (attempt ${attempt + 1}/${maxAttempts})`,
108+
);
109+
await delay(INSTALL_RETRY_DELAY_MS);
110+
}
125111
}
126112
}
113+
return false;
114+
};
115+
116+
const ensureBinInPath = (): void => {
117+
const binDir = join(getVitePlusHome(platform), "bin");
118+
if (!targetEnv.PATH?.includes(binDir)) {
119+
const separator = platform === "win32" ? ";" : ":";
120+
targetEnv.PATH = `${binDir}${separator}${targetEnv.PATH || ""}`;
121+
prependPath?.(binDir);
122+
}
123+
};
124+
125+
if (pinned.length > 0) {
126+
if (await tryUrls(pinned)) {
127+
ensureBinInPath();
128+
return;
129+
}
130+
warn(
131+
`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}.`,
132+
);
133+
}
134+
135+
if (await tryUrls(latest)) {
136+
ensureBinInPath();
137+
return;
127138
}
128139

129140
throw new Error(
130-
`Failed to install Vite+ after ${maxAttempts} attempts across ${urls.length} URL(s): ${failureReason}`,
141+
`Failed to install Vite+ after ${maxAttempts} attempts across ${totalUrls} URL(s): ${failureReason}`,
131142
);
132143
}

src/ci/install-script-urls.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect } from "vite-plus/test";
2+
import { getInstallScriptUrls, pkgPrNewCommitSha } from "./install-script-urls.js";
3+
4+
const commitSha = "7d848b3da1987fa60b4cf18487fcc36a2a697e94";
5+
6+
describe("pkgPrNewCommitSha", () => {
7+
it("extracts the SHA from a pkg.pr.new commit build", () => {
8+
expect(pkgPrNewCommitSha(`0.0.0-commit.${commitSha}`)).toBe(commitSha);
9+
});
10+
11+
it("returns undefined for regular versions and near-miss SHA lengths", () => {
12+
expect(pkgPrNewCommitSha("0.2.9")).toBeUndefined();
13+
expect(pkgPrNewCommitSha(`0.0.0-commit.${commitSha.slice(0, 39)}`)).toBeUndefined();
14+
});
15+
});
16+
17+
describe("getInstallScriptUrls", () => {
18+
it("pins an exact version to its release tag, with jsDelivr as mirror", () => {
19+
const { pinned, latest } = getInstallScriptUrls("0.2.9", "linux");
20+
expect(pinned).toEqual([
21+
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/v0.2.9/packages/cli/install.sh",
22+
"https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@v0.2.9/packages/cli/install.sh",
23+
]);
24+
expect(latest).toEqual([
25+
"https://viteplus.dev/install.sh",
26+
"https://raw.githubusercontent.com/voidzero-dev/vite-plus/main/packages/cli/install.sh",
27+
]);
28+
});
29+
30+
it("pins prerelease versions to their release tag", () => {
31+
const { pinned } = getInstallScriptUrls("0.1.21-alpha.7", "linux");
32+
expect(pinned[0]).toContain("/v0.1.21-alpha.7/");
33+
});
34+
35+
it("pins pkg.pr.new commit builds to the commit, lowercasing the SHA", () => {
36+
const { pinned } = getInstallScriptUrls(`0.0.0-commit.${commitSha.toUpperCase()}`, "linux");
37+
expect(pinned).toEqual([
38+
`https://raw.githubusercontent.com/voidzero-dev/vite-plus/${commitSha}/packages/cli/install.sh`,
39+
`https://cdn.jsdelivr.net/gh/voidzero-dev/vite-plus@${commitSha}/packages/cli/install.sh`,
40+
]);
41+
});
42+
43+
it.each(["latest", "next", "^0.2.0", "0.2", "0.2.x", "0.2.9+build.5", ""])(
44+
"does not pin %j",
45+
(version) => {
46+
const { pinned, latest } = getInstallScriptUrls(version, "linux");
47+
expect(pinned).toEqual([]);
48+
expect(latest).toHaveLength(2);
49+
},
50+
);
51+
52+
it("uses install.ps1 on Windows for both pinned and latest URLs", () => {
53+
const { pinned, latest } = getInstallScriptUrls("0.2.9", "win32");
54+
for (const url of [...pinned, ...latest]) {
55+
expect(url).toMatch(/install\.ps1$/);
56+
}
57+
});
58+
});

0 commit comments

Comments
 (0)