From bf34f76d2bc58e74ae753cdd98f9eac98f261fa7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 22 Oct 2026 02:52:09 -0700 Subject: [PATCH] fix(install): stable installs no longer fail behind a page of nightlies The install scripts read one page of 100 GitHub releases, so a stable install failed once 100 nightlies were newer than the latest stable release. Stable now reads github.com//releases/latest as JSON. release.yml marks only plain stable releases latest, and this page is not under the anonymous API quota. Nightly and preview page the releases API, up to 10 pages. Co-authored-by: Victor Vogel Co-Authored-By: Claude Opus 5.5 --- scripts/install.ps1 | 15 ++++++-- scripts/install.sh | 20 ++++++++--- scripts/install.test.ts | 80 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 scripts/install.test.ts diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 58df4a6447abc..a263cef0835a2 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -139,8 +139,19 @@ if (-not $version) { "preview" { '^v\d+\.\d+\.\d+-preview\.\d+\.\d+$' } default { Fail "T3CODE_CHANNEL must be stable, nightly, or preview" } } - $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers @{ "User-Agent" = "t3-install" } - $tag = ($releases | Where-Object { -not $_.draft -and $_.tag_name -match $tagPattern } | Select-Object -First 1).tag_name + if ($channel -eq "stable") { + # release.yml marks only plain stable releases latest. This is a github.com + # page, so it is not cut off by a page of newer nightlies or the API quota. + $release = Invoke-RestMethod -Uri "https://github.com/$repo/releases/latest" -Headers @{ "User-Agent" = "t3-install"; "Accept" = "application/json" } + $tag = ($release | Where-Object { $_.tag_name -match $tagPattern }).tag_name + } else { + $tag = $null + for ($page = 1; -not $tag -and $page -le 10; $page++) { + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100&page=$page" -Headers @{ "User-Agent" = "t3-install" } + $tag = ($releases | Where-Object { -not $_.draft -and $_.tag_name -match $tagPattern } | Select-Object -First 1).tag_name + if (-not $releases) { break } + } + } if (-not $tag) { Fail "could not find a $channel release; set T3CODE_VERSION" } $version = $tag.Substring(1) } diff --git a/scripts/install.sh b/scripts/install.sh index 1718aff952eda..82b0e4b41d537 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -53,14 +53,14 @@ step "Finding your release..." # Exit 44 on a 404 so callers can tell "no such asset" from a network failure. fetch() { if command -v curl >/dev/null 2>&1; then - status="$(curl -sSL -w '%{http_code}' "$1" -o "$2")" || return 1 + status="$(curl -sSL -H "Accept: ${3:-*/*}" -w '%{http_code}' "$1" -o "$2")" || return 1 case "$status" in 2??) return 0 ;; 404) return 44 ;; *) printf 'GET %s returned HTTP %s\n' "$1" "$status" >&2; return 1 ;; esac elif command -v wget >/dev/null 2>&1; then - wget -q --server-response "$1" -O "$2" 2>"$2.headers" && rm -f "$2.headers" && return 0 + wget -q --server-response --header="Accept: ${3:-*/*}" "$1" -O "$2" 2>"$2.headers" && rm -f "$2.headers" && return 0 if grep -q ' 404 ' "$2.headers" 2>/dev/null; then rm -f "$2.headers"; return 44; fi cat "$2.headers" >&2; rm -f "$2.headers"; return 1 else @@ -154,8 +154,20 @@ if [ -z "$version" ]; then *) fail "T3CODE_CHANNEL must be stable, nightly, or preview" ;; esac tmp_index="$(mktemp)" - fetch "https://api.github.com/repos/${repo}/releases?per_page=100" "$tmp_index" - version="$(sed -n "s/.*\"tag_name\": *\"${tag_pattern}\".*/\1/p" "$tmp_index" | head -n 1)" + if [ "$channel" = stable ]; then + # release.yml marks only plain stable releases latest. This is a github.com + # page, so it is not cut off by a page of newer nightlies or the API quota. + fetch "https://github.com/${repo}/releases/latest" "$tmp_index" application/json + version="$(sed -n "s/.*\"tag_name\": *\"${tag_pattern}\".*/\1/p" "$tmp_index" | head -n 1)" + else + page=1 + while [ -z "$version" ] && [ "$page" -le 10 ]; do + fetch "https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}" "$tmp_index" + version="$(sed -n "s/.*\"tag_name\": *\"${tag_pattern}\".*/\1/p" "$tmp_index" | head -n 1)" + grep -q '"tag_name"' "$tmp_index" || break + page=$((page + 1)) + done + fi rm -f "$tmp_index" [ -n "$version" ] || fail "could not find a ${channel} release; set T3CODE_VERSION" fi diff --git a/scripts/install.test.ts b/scripts/install.test.ts new file mode 100644 index 0000000000000..5be7acc8e004d --- /dev/null +++ b/scripts/install.test.ts @@ -0,0 +1,80 @@ +// @effect-diagnostics nodeBuiltinImport:off - Drives the real shell installer against a local GitHub fixture. +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as NodeChildProcess from "node:child_process"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeHttp from "node:http"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import { describe, expect, it } from "vite-plus/test"; + +const nightlies = Array.from({ length: 100 }, (_, index) => ({ + tag_name: `v1.2.4-nightly.20261020.${200 - index}`, +})); + +// regression: #22490 +describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer channel lookup", () => { + it.each([ + ["stable", "1.2.3"], + ["preview", "1.2.4-preview.20261019.50"], + ])("finds the newest %s release behind a full page of nightlies", async (channel, version) => { + const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-index-")); + const target = NodePath.join(root, "home/runtime/versions", version); + const server = NodeHttp.createServer((request, response) => { + const url = new URL(request.url ?? "/", "http://fixture"); + const body = + url.pathname === "/pingdotgg/t3code/releases/latest" && + request.headers.accept === "application/json" + ? { tag_name: "v1.2.3" } + : url.pathname === "/repos/pingdotgg/t3code/releases" + ? { "1": nightlies, "2": [{ tag_name: `v${version}` }, { tag_name: "v1.2.3" }] }[ + url.searchParams.get("page") ?? "" + ] + : undefined; + if (body === undefined) response.statusCode = 404; + // The API pretty-prints and the installer reads it line by line. + response.end(JSON.stringify(body ?? [], null, 2)); + }); + try { + // An existing completed install isolates release resolution from archive downloads. + await NodeFSP.mkdir(target, { recursive: true }); + await NodeFSP.writeFile(NodePath.join(target, ".install-complete"), `${version}\n`); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a TCP listener"); + const origin = `http://127.0.0.1:${address.port}`; + const source = await NodeFSP.readFile( + NodePath.resolve(import.meta.dirname, "install.sh"), + "utf8", + ); + const installer = NodePath.join(root, "install.sh"); + await NodeFSP.writeFile(installer, source.replace(/https:\/\/(api\.)?github\.com/g, origin)); + const { code, output } = await new Promise<{ code: number | null; output: string }>( + (resolve, reject) => { + const child = NodeChildProcess.spawn("sh", [installer], { + env: { + ...process.env, + T3CODE_VERSION: "", + T3CODE_CHANNEL: channel, + T3CODE_HOME: NodePath.join(root, "home"), + T3CODE_INSTALL_BIN_DIR: NodePath.join(root, "bin"), + // A wrong version must fail here instead of downloading a real release. + T3CODE_RELEASE_BASE_URL: origin, + }, + stdio: ["ignore", "pipe", "pipe"], + }); + let output = ""; + child.stdout.on("data", (chunk: Buffer) => (output += chunk.toString())); + child.stderr.on("data", (chunk: Buffer) => (output += chunk.toString())); + child.on("error", reject); + child.on("close", (code) => resolve({ code, output })); + }, + ); + expect(code, output).toBe(0); + expect(output).toContain(`Installed T3 Code ${version}`); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + await NodeFSP.rm(root, { recursive: true, force: true }); + } + }); +});