diff --git a/apps/server/src/cli/update.test.ts b/apps/server/src/cli/update.test.ts index 67ae3bfa5321..48c7c3a4f44c 100644 --- a/apps/server/src/cli/update.test.ts +++ b/apps/server/src/cli/update.test.ts @@ -10,15 +10,68 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import { Command } from "effect/cli"; +import { HttpClient, HttpClientResponse } from "effect/http"; import { afterEach, vi } from "vite-plus/test"; import packageJson from "../../package.json" with { type: "json" }; import * as BootService from "../cloud/bootService.ts"; import { pinnedRuntimePaths } from "../cloud/pinnedRuntime.ts"; -import { repointLauncher, resolveLauncherPath, updateCommand } from "./update.ts"; +import { + repointLauncher, + resolveLauncherPath, + resolveNewestVersion, + updateCommand, +} from "./update.ts"; afterEach(() => vi.restoreAllMocks()); +it.effect.each([ + { environment: { GH_TOKEN: " gh-token ", GITHUB_TOKEN: "github-token" }, token: "gh-token" }, + { environment: { GH_TOKEN: " ", GITHUB_TOKEN: " github-token " }, token: "github-token" }, + { environment: { GITHUB_TOKEN: "github-token" }, token: "github-token" }, + { environment: { GH_TOKEN: " ", GITHUB_TOKEN: " " }, token: undefined }, + { environment: {}, token: undefined }, +])("selects release authentication from $environment", ({ environment, token }) => + Effect.gen(function* () { + const authorizations: Array = []; + const httpClient = HttpClient.make((request) => { + authorizations.push(request.headers.authorization); + return Effect.succeed( + HttpClientResponse.fromWeb(request, Response.json([{ tag_name: "v1.2.3" }])), + ); + }); + const version = yield* resolveNewestVersion("stable").pipe( + Effect.provideService(HostProcess.Environment, environment), + Effect.provideService(HttpClient.HttpClient, httpClient), + ); + assert.equal(version, "1.2.3"); + assert.deepStrictEqual(authorizations, [token === undefined ? undefined : `Bearer ${token}`]); + }), +); + +it.effect("authenticates each release page until the requested channel is found", () => + Effect.gen(function* () { + const pages: string[] = []; + const httpClient = HttpClient.make((request) => { + const url = new URL(request.url); + assert.equal(url.origin, "https://api.github.com"); + assert.equal(url.pathname, "/repos/pingdotgg/t3code/releases"); + assert.equal(request.headers.accept, "application/vnd.github+json"); + assert.equal(request.headers.authorization, "Bearer gh-token"); + const page = url.searchParams.get("page") ?? ""; + pages.push(page); + const tag_name = page === "1" ? "v1.2.4-preview.20260101.1" : "v1.2.3"; + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json([{ tag_name }]))); + }); + const version = yield* resolveNewestVersion("stable").pipe( + Effect.provideService(HostProcess.Environment, { GH_TOKEN: "gh-token" }), + Effect.provideService(HttpClient.HttpClient, httpClient), + ); + assert.equal(version, "1.2.3"); + assert.deepStrictEqual(pages, ["1", "2"]); + }), +); + it.layer(NodeServices.layer)("t3 update launcher", (it) => { it.effect("repoints a symlink that lives in a runtime versions tree", () => Effect.gen(function* () { diff --git a/apps/server/src/cli/update.ts b/apps/server/src/cli/update.ts index 3d18f8015a29..9a35a47424e4 100644 --- a/apps/server/src/cli/update.ts +++ b/apps/server/src/cli/update.ts @@ -52,27 +52,29 @@ const decodeReleaseIndex = Schema.decodeUnknownEffect(Schema.fromJsonString(Rele const RELEASE_INDEX_TIMEOUT = Duration.seconds(30); const RELEASE_INDEX_MAX_PAGES = 10; -const resolveNewestVersion = Effect.fn("cli.update.resolve_newest")(function* ( +export const resolveNewestVersion = Effect.fn("cli.update.resolve_newest")(function* ( channel: CliReleaseChannel, ) { const httpClient = yield* HttpClient.HttpClient; + const environment = yield* HostProcess.Environment; + const githubToken = environment.GH_TOKEN?.trim() || environment.GITHUB_TOKEN?.trim() || undefined; for (let page = 1; page <= RELEASE_INDEX_MAX_PAGES; page += 1) { - const body = yield* httpClient - .execute( - HttpClientRequest.get(cliReleaseIndexPageUrl(page)).pipe( - HttpClientRequest.setHeader("Accept", "application/vnd.github+json"), - ), - ) - .pipe( - Effect.flatMap(HttpClientResponse.filterStatusOk), - Effect.flatMap((response) => response.text), - Effect.mapError(() => new CliUpdateError({ reason: "Could not list t3 releases." })), - Effect.timeoutOrElse({ - duration: RELEASE_INDEX_TIMEOUT, - orElse: () => - Effect.fail(new CliUpdateError({ reason: "Timed out listing t3 releases." })), - }), - ); + const request = HttpClientRequest.get(cliReleaseIndexPageUrl(page)).pipe( + HttpClientRequest.setHeader("Accept", "application/vnd.github+json"), + ); + const authenticatedRequest = + githubToken === undefined + ? request + : HttpClientRequest.setHeader(request, "Authorization", `Bearer ${githubToken}`); + const body = yield* httpClient.execute(authenticatedRequest).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.flatMap((response) => response.text), + Effect.mapError(() => new CliUpdateError({ reason: "Could not list t3 releases." })), + Effect.timeoutOrElse({ + duration: RELEASE_INDEX_TIMEOUT, + orElse: () => Effect.fail(new CliUpdateError({ reason: "Timed out listing t3 releases." })), + }), + ); const releases = yield* decodeReleaseIndex(body).pipe( Effect.mapError( () => new CliUpdateError({ reason: "The t3 release index had an unexpected shape." }),