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
80 changes: 75 additions & 5 deletions apps/server/src/cli/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import * as BootService from "../cloud/bootService.ts";
import { prunePinnedRuntimes } from "../cloud/pinnedRuntime.ts";
import * as ManagedPolicy from "../policy/ManagedPolicy.ts";
import * as ProcessRunner from "../processRunner.ts";
import { repointLauncher, resolveLauncherPath, resolveNewestVersion, runUpdate } from "./update.ts";
import {
repointLauncher,
resolveLauncherPath,
resolveMirrorVersion,
resolveNewestVersion,
runUpdate,
} from "./update.ts";

// lsof listing one process, so prune sees a process list in which no runtime is running.
const unrelatedLsof = ProcessRunner.ProcessRunner.of({
Expand Down Expand Up @@ -60,10 +66,59 @@ it.effect("authenticates each release page until the requested channel is found"
}),
);

it.effect("reads the newest version from the update feed file a mirror publishes", () =>
Effect.gen(function* () {
const requested: string[] = [];
const mirror = {
organization: "Example Corp",
feedUrl: "https://updates.example.com/t3code/?token=abc",
};
const resolve = (
channel: "stable" | "nightly" | "preview",
platform: NodeJS.Platform,
arch: string,
feed: string,
) =>
resolveMirrorVersion(mirror, channel).pipe(
Effect.map((version): string => version),
Effect.catchTags({ CliUpdateError: (error) => Effect.succeed(`error: ${error.reason}`) }),
Effect.provideService(HostProcess.Platform, platform),
Effect.provideService(HostProcess.Architecture, arch),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
requested.push(request.url);
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(feed)));
}),
),
);

assert.equal(
yield* resolve("stable", "darwin", "arm64", "version: 1.4.0\nfiles: []\n"),
"1.4.0",
);
assert.equal(
yield* resolve("nightly", "linux", "arm64", "version: '1.5.0-nightly.20261101.2'\n"),
"1.5.0-nightly.20261101.2",
);
assert.deepStrictEqual(requested, [
"https://updates.example.com/t3code/latest-mac.yml?token=abc",
"https://updates.example.com/t3code/nightly-linux-arm64.yml?token=abc",
]);
// A feed for the wrong track is not trusted to mean the requested one.
assert.include(
yield* resolve("stable", "win32", "x64", "version: 1.5.0-nightly.20261101.2\n"),
"Could not read latest.yml from Example Corp's update mirror.",
);
assert.include(yield* resolve("preview", "win32", "x64", ""), "has no preview builds");
}),
);

it.layer(NodeServices.layer)("t3 update under a managed policy", (it) => {
const runWithPolicy = (
updates: ManagedUpdatesPolicy,
input: { readonly channel?: "stable" | "nightly"; readonly requestedVersion?: string } = {},
httpClient = HttpClient.make(() => Effect.die("unexpected release lookup")),
) =>
runUpdate({
baseDir: "/unused",
Expand All @@ -85,10 +140,7 @@ it.layer(NodeServices.layer)("t3 update under a managed policy", (it) => {
// Refusals happen before the update touches the network, the service, or a process.
Layer.mock(BootService.BootService, {}),
Layer.mock(ProcessRunner.ProcessRunner, {}),
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("unexpected release lookup")),
),
Layer.succeed(HttpClient.HttpClient, httpClient),
),
),
);
Expand Down Expand Up @@ -118,6 +170,24 @@ it.layer(NodeServices.layer)("t3 update under a managed policy", (it) => {
}
}),
);

it.effect("looks for updates on the policy's mirror, never the public releases", () =>
Effect.gen(function* () {
const requested: string[] = [];
const error = yield* runWithPolicy(
{ mode: "auto", feedUrl: "https://updates.example.com/t3code" },
{ channel: "stable" },
HttpClient.make((request) => {
requested.push(new URL(request.url).origin);
return Effect.succeed(
HttpClientResponse.fromWeb(request, new Response("", { status: 404 })),
);
}),
);
assert.include(error.message, "from Example Corp's update mirror");
assert.deepStrictEqual(requested, ["https://updates.example.com"]);
}),
);
});

it.layer(NodeServices.layer)("t3 update launcher", (it) => {
Expand Down
78 changes: 62 additions & 16 deletions apps/server/src/cli/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
cliReleaseIndexPageUrl,
cliReleaseChannelOf,
newestCliReleaseVersion,
releaseAssetUrl,
updateFeedFileName,
type CliReleaseChannel,
} from "@t3tools/shared/cliRelease";
import * as Console from "effect/Console";
Expand All @@ -24,6 +26,7 @@ import {
ensurePinnedRuntimeInstalled,
launcherOwnsVersionsDir,
pinnedRuntimeCommand,
isUnpublishedRelease,
PinnedRuntimeInstallError,
pinnedRuntimePaths,
pinnedRuntimeVersionsDir,
Expand All @@ -33,7 +36,7 @@ import {
} from "../cloud/pinnedRuntime.ts";
import { compareExactServiceVersions, isExactServiceVersion } from "../cloud/serviceProtocol.ts";
import * as ManagedPolicy from "../policy/ManagedPolicy.ts";
import { managedUpdateRefusal } from "../policy/managedUpdates.ts";
import { managedUpdateMirror, managedUpdateRefusal } from "../policy/managedUpdates.ts";
import * as ProcessRunner from "../processRunner.ts";
import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
Expand Down Expand Up @@ -94,6 +97,47 @@ export const resolveNewestVersion = Effect.fn("cli.update.resolve_newest")(funct
return yield* new CliUpdateError({ reason: `No published ${channel} release was found.` });
});

/** The version an update mirror's feed file for this platform offers on `channel`. */
export const resolveMirrorVersion = Effect.fn("cli.update.resolve_mirror")(function* (
mirror: { readonly organization: string; readonly feedUrl: string },
channel: CliReleaseChannel,
) {
if (channel === "preview") {
return yield* new CliUpdateError({
reason: `${mirror.organization}'s update mirror has no preview builds.`,
});
}
const httpClient = yield* HttpClient.HttpClient;
const fileName = updateFeedFileName(
channel === "nightly" ? "nightly" : "latest",
yield* HostProcess.Platform,
yield* HostProcess.Architecture,
);
const url = releaseAssetUrl(mirror.feedUrl, fileName);
// Not the URL, whose query can hold the mirror's access token.
const unreadable = new CliUpdateError({
reason: `Could not read ${fileName} from ${mirror.organization}'s update mirror.`,
});
const body = yield* httpClient.get(url).pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap((response) => response.text),
Effect.mapError(() => unreadable),
Effect.timeoutOrElse({
duration: RELEASE_INDEX_TIMEOUT,
orElse: () => Effect.fail(unreadable),
}),
);
const version = /^version:\s*["']?([^"'\s]+)/m.exec(body)?.[1];
if (
version === undefined ||
!isExactServiceVersion(version) ||
cliReleaseChannelOf(version) !== channel
) {
return yield* unreadable;
}
return version;
});

/**
* Only a launcher that already points into this home's `runtime/versions` tree is touched; a
* plain copy of the executable, or a launcher for some other install, is left alone. It is
Expand Down Expand Up @@ -320,6 +364,7 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: {
const httpClient = yield* HttpClient.HttpClient;
const service = yield* BootService.BootService;
const policy = yield* ManagedPolicy.ManagedPolicy;
const mirror = managedUpdateMirror(policy.document);

const currentVersion = packageJson.version;
const channel = input.channel ?? cliReleaseChannelOf(currentVersion);
Expand All @@ -338,9 +383,11 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: {
const progress = createUpdateProgress();
progress.status("Checking for updates...");
const targetVersion = yield* (
input.requestedVersion === undefined
? resolveNewestVersion(channel)
: Effect.succeed(input.requestedVersion)
input.requestedVersion !== undefined
? Effect.succeed(input.requestedVersion)
: mirror === undefined
? resolveNewestVersion(channel)
: resolveMirrorVersion(mirror, channel)
).pipe(Effect.ensuring(Effect.sync(progress.finish)));
const targetChannel = cliReleaseChannelOf(targetVersion);

Expand Down Expand Up @@ -455,7 +502,8 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: {
httpClient,
platform,
arch,
releaseBaseUrl: environment[CLI_RELEASE_BASE_URL_ENV]?.trim() || undefined,
releaseBaseUrl: mirror?.feedUrl ?? (environment[CLI_RELEASE_BASE_URL_ENV]?.trim() || undefined),
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
requirePublishedRelease: mirror !== undefined,
Comment thread
juliusmarminge marked this conversation as resolved.
Resolved
validate: (paths) =>
runner
.run({
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand All @@ -481,17 +529,15 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: {
),
}).pipe(
Effect.ensuring(Effect.sync(progress.finish)),
Effect.catchIf(
(error): error is PinnedRuntimeInstallError =>
error._tag === "PinnedRuntimeInstallError" &&
error.step.startsWith("downloading the t3 release checksums") &&
String(error.cause).includes("404"),
() =>
Effect.fail(
new CliUpdateError({
reason: `No release archive was published for t3@${targetVersion}.`,
}),
),
Effect.catchIf(isUnpublishedRelease, () =>
Effect.fail(
new CliUpdateError({
reason:
mirror === undefined
? `No release archive was published for t3@${targetVersion}.`
: `${mirror.organization}'s update mirror has no t3@${targetVersion} for this machine.`,
}),
),
),
Comment thread
juliusmarminge marked this conversation as resolved.
Resolved
);

Expand Down
73 changes: 72 additions & 1 deletion apps/server/src/cloud/pinnedRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess";
import * as ProcessRunner from "../processRunner.ts";
import {
ensurePinnedRuntimeInstalled,
isUnpublishedRelease,
pinnedRuntimeCommand,
pinnedRuntimePaths,
PinnedRuntimeInstallError,
Expand All @@ -39,7 +40,7 @@ const validChecksums = archiveHex(archiveBytes).pipe(
const releaseHttpClient = (checksums: string, requests: string[] = []) =>
HttpClient.make((request) => {
requests.push(request.url);
const body = request.url.endsWith("/SHA256SUMS") ? checksums : archiveBytes;
const body = new URL(request.url).pathname.endsWith("/SHA256SUMS") ? checksums : archiveBytes;
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(body)));
});
const extractingRunner = (fs: FileSystem.FileSystem, path: Path.Path, commands: string[] = []) =>
Expand Down Expand Up @@ -155,6 +156,40 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => {
}),
);

it.effect("reports a release that lists an archive it does not serve as unpublished", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-archive-missing-" });
const checksums = yield* validChecksums;
const failsWith = (status: number) =>
ensurePinnedRuntimeInstalled({
baseDir,
version,
fs,
path,
platform: "linux",
arch: "x64",
httpClient: HttpClient.make((request) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
request.url.endsWith("/SHA256SUMS")
? new Response(checksums)
: new Response("", { status }),
),
),
),
// "404" in the mirror's path must not read a server error as a missing release.
releaseBaseUrl: "https://updates.example.com/404/t3code",
runner: extractingRunner(fs, path),
validate: () => Effect.die("must not validate a missing archive"),
}).pipe(Effect.flip);
assert.isTrue(isUnpublishedRelease(yield* failsWith(404)));
assert.isFalse(isUnpublishedRelease(yield* failsWith(500)));
}),
);

it.effect("refuses an archive whose checksum does not match the release", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -267,6 +302,42 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => {
}),
);

it.effect("reuses a cached runtime under a mirror only while the mirror lists it", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-runtime-mirror-" });
const cached = pinnedRuntimePaths(path, baseDir, version, "linux");
yield* fs.makeDirectory(path.dirname(cached.entryPath), { recursive: true });
yield* fs.writeFileString(cached.entryPath, "#!/bin/sh\n");
yield* fs.writeFileString(cached.sentinelPath, `${version}\n`);
const install = (checksums: string, requests: string[]) =>
ensurePinnedRuntimeInstalled({
baseDir,
version,
fs,
path,
platform: "linux",
arch: "x64",
httpClient: releaseHttpClient(checksums, requests),
// A signed mirror URL keeps its query on every file it serves.
releaseBaseUrl: "https://updates.example.com/t3code/?token=abc",
requirePublishedRelease: true,
runner: extractingRunner(fs, path),
validate: () => Effect.void,
});

const withdrawn = yield* install("", []).pipe(Effect.flip);
assert.include(withdrawn.message, `finding ${archiveName} in the t3 release checksums`);
const requests: string[] = [];
assert.equal((yield* install(yield* validChecksums, requests)).entryPath, cached.entryPath);
// Listed, so the cached runtime is used without downloading it again.
assert.deepEqual(requests, [
`https://updates.example.com/t3code/v${version}/SHA256SUMS?token=abc`,
]);
}),
);

it.effect("preserves a completed runtime when validation fails", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
Loading
Loading