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
55 changes: 54 additions & 1 deletion apps/server/src/cli/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> = [];
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* () {
Expand Down
36 changes: 19 additions & 17 deletions apps/server/src/cli/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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." }),
Expand Down
Loading