Skip to content
Closed
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
10 changes: 6 additions & 4 deletions cli/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { VERSION } from "@/version.ts";
import { asCliArgString } from "@/lib/cli-args.ts";
import { writeCommandOutput } from "@/lib/command-output.ts";
import { defineAltertableCommand } from "@/lib/command-context.ts";
import { CliError } from "@/lib/errors.ts";
import { GLOBAL_ARGV_FLAGS_WITH_VALUE, isGlobalArgvFlag } from "@/lib/global-flags.ts";
import { getInstalledVersion } from "@/lib/installed-version.ts";
import type { OutputSink } from "@/lib/runtime.ts";
import {
checkForUpdate,
Expand Down Expand Up @@ -76,9 +76,9 @@ function buildTargetResult(targetVersion: string): UpdateCheckResult {
}
const source = resolveUpdateSource();
return {
current_version: VERSION,
current_version: getInstalledVersion(),
latest_version: version,
update_available: compareVersions(version, VERSION) > 0,
update_available: compareVersions(version, getInstalledVersion()) > 0,
source,
release_url: releaseUrlForSource(source, version),
checked_at: new Date().toISOString(),
Expand Down Expand Up @@ -121,7 +121,9 @@ export async function executeUpdateCommand(
targetVersion &&
compareVersions(result.latest_version, result.current_version) < 0
) {
throw new CliError(`Target version v${result.latest_version} is older than v${VERSION}.`);
throw new CliError(
`Target version v${result.latest_version} is older than v${getInstalledVersion()}.`,
);
}
if (!args.force && !result.update_available) {
await writeUpdateCheck(result, targetVersion, sink);
Expand Down
25 changes: 25 additions & 0 deletions cli/src/lib/installed-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { VERSION } from "@/version.ts";

let installedVersionOverride: string | undefined;

export function getInstalledVersion(): string {
return installedVersionOverride ?? VERSION;
}

export function withInstalledVersion<T>(version: string, fn: () => T): T {
const previous = installedVersionOverride;
installedVersionOverride = version;
try {
const result = fn();
if (result instanceof Promise) {
return result.finally(() => {
installedVersionOverride = previous;
}) as T;
}
installedVersionOverride = previous;
return result;
} catch (error) {
installedVersionOverride = previous;
throw error;
}
}
14 changes: 9 additions & 5 deletions cli/src/lib/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { createHash, randomBytes } from "node:crypto";
import { spawnSync } from "node:child_process";
import type { CliContext } from "@/context.ts";
import { isJsonOutput } from "@/context.ts";
import { USER_AGENT, VERSION } from "@/version.ts";
import { USER_AGENT } from "@/version.ts";
import { getInstalledVersion } from "@/lib/installed-version.ts";
import { configDir, configGetGlobal, configSetGlobal } from "@/lib/config.ts";
import { urlencode } from "@/lib/encode.ts";
import { CliError, HttpError, NetworkError } from "@/lib/errors.ts";
Expand Down Expand Up @@ -1051,9 +1052,9 @@ export async function checkForUpdate(options: FetchLatestOptions = {}): Promise<
const release = await fetchLatestRelease(options);
const checkedAt = new Date().toISOString();
const result: UpdateCheckResult = {
current_version: VERSION,
current_version: getInstalledVersion(),
latest_version: release.version,
update_available: compareVersions(release.version, VERSION) > 0,
update_available: compareVersions(release.version, getInstalledVersion()) > 0,
source: release.source,
release_url: release.releaseUrl,
checked_at: checkedAt,
Expand Down Expand Up @@ -1141,7 +1142,10 @@ function writeAutomaticUpdateNotice(sink: OutputSink, latestVersion: string): vo
sink.writeMetadata([
"",
renderDisplayText([
span(`Update available: altertable ${latestVersion} (current ${VERSION}).`, "subtle"),
span(
`Update available: altertable ${latestVersion} (current ${getInstalledVersion()}).`,
"subtle",
),
]),
renderDisplayText([
span(`Run ${recommendedInstallCommand(latestVersion)} to install it.`, "subtle"),
Expand Down Expand Up @@ -1177,7 +1181,7 @@ export async function maybeShowUpdateNotice(options: AutomaticNoticeOptions): Pr
}
}

if (latestVersion && compareVersions(latestVersion, VERSION) > 0) {
if (latestVersion && compareVersions(latestVersion, getInstalledVersion()) > 0) {
writeAutomaticUpdateNotice(sink, latestVersion);
}
} catch {
Expand Down
130 changes: 74 additions & 56 deletions cli/tests/updater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CLI_PACKAGE_METADATA } from "@/package-metadata.ts";
import { VERSION } from "@/version.ts";
import { ConfigurationError } from "@/lib/errors.ts";
import { resolveProcessExecutablePath } from "@/lib/executable-path.ts";
import { withInstalledVersion } from "@/lib/installed-version.ts";
import {
checkForUpdate,
compareVersions,
Expand Down Expand Up @@ -47,6 +48,9 @@ import { createCliRuntime, runWithCliRuntime } from "@/lib/runtime.ts";
let testHome = "";
const packageJsonPath = resolve(import.meta.dir, "../package.json");
const UPDATE_TEST_VERSION = "1.2.3";
// Pin the "installed" version well below UPDATE_TEST_VERSION so update checks
// stay meaningful regardless of what release-please stamps into version.ts.
const INSTALLED_TEST_VERSION = "1.0.0";
const LINUX_X64_ASSET = "altertable-linux-x64";
const LINUX_X64_DOWNLOAD_URL = `https://download.example/${LINUX_X64_ASSET}`;
const CHECKSUMS_DOWNLOAD_URL = "https://download.example/checksums.txt";
Expand Down Expand Up @@ -303,13 +307,15 @@ describe("release discovery", () => {
});

test("checkForUpdate writes cached state", async () => {
const result = await checkForUpdate({
source: "npm",
fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }),
});
await withInstalledVersion(INSTALLED_TEST_VERSION, async () => {
const result = await checkForUpdate({
source: "npm",
fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }),
});

expect(result.update_available).toBe(true);
expect(readUpdateState().latest_version).toBe(UPDATE_TEST_VERSION);
expect(result.update_available).toBe(true);
expect(readUpdateState().latest_version).toBe(UPDATE_TEST_VERSION);
});
});
});

Expand Down Expand Up @@ -696,14 +702,16 @@ describe("automatic update checks", () => {
stderr.push(...lines);
};

await runWithCliRuntime(runtime, async () => {
await maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "context",
stderrIsTTY: true,
fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }),
});
});
await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
runWithCliRuntime(runtime, async () => {
await maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "context",
stderrIsTTY: true,
fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }),
});
}),
);

expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`);
expect(stderr.join("\n")).toContain("altertable update");
Expand All @@ -714,26 +722,28 @@ describe("automatic update checks", () => {
const stderr: string[] = [];
let fetchCalls = 0;

await maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "help",
stderrIsTTY: true,
fetchImpl: (async () => {
fetchCalls += 1;
throw new Error("cached notices should not fetch");
}) as unknown as typeof fetch,
sink: {
json: false,
debug: false,
writeStderr() {},
writeJson() {},
writeRaw() {},
writeHuman() {},
writeMetadata(lines) {
stderr.push(...lines);
await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "help",
stderrIsTTY: true,
fetchImpl: (async () => {
fetchCalls += 1;
throw new Error("cached notices should not fetch");
}) as unknown as typeof fetch,
sink: {
json: false,
debug: false,
writeStderr() {},
writeJson() {},
writeRaw() {},
writeHuman() {},
writeMetadata(lines) {
stderr.push(...lines);
},
},
},
});
}),
);

expect(fetchCalls).toBe(0);
expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`);
Expand All @@ -744,26 +754,28 @@ describe("automatic update checks", () => {
const state = readUpdateState();
const stderr: string[] = [];

await maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "context",
stderrIsTTY: true,
now: new Date(Date.parse(state.last_checked_at ?? "") + 24 * 60 * 60 * 1000),
fetchImpl: (async () => {
throw new Error("network unavailable");
}) as unknown as typeof fetch,
sink: {
json: false,
debug: false,
writeStderr() {},
writeJson() {},
writeRaw() {},
writeHuman() {},
writeMetadata(lines) {
stderr.push(...lines);
await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
maybeShowUpdateNotice({
context: { debug: false, json: false, agent: false },
commandName: "context",
stderrIsTTY: true,
now: new Date(Date.parse(state.last_checked_at ?? "") + 24 * 60 * 60 * 1000),
fetchImpl: (async () => {
throw new Error("network unavailable");
}) as unknown as typeof fetch,
sink: {
json: false,
debug: false,
writeStderr() {},
writeJson() {},
writeRaw() {},
writeHuman() {},
writeMetadata(lines) {
stderr.push(...lines);
},
},
},
});
}),
);

expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`);
expect(readUpdateState().last_error).toBe("Unable to check for CLI updates.");
Expand All @@ -772,11 +784,13 @@ describe("automatic update checks", () => {

describe("update command", () => {
test("checks an explicit target version without network", async () => {
const output = await runUpdateCommand(["update", UPDATE_TEST_VERSION, "--check"]);
const output = await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
runUpdateCommand(["update", UPDATE_TEST_VERSION, "--check"]),
);

expect(output.stdout[0]).toBe(
[
`A new version of altertable is available: v${UPDATE_TEST_VERSION} (current v${VERSION})`,
`A new version of altertable is available: v${UPDATE_TEST_VERSION} (current v${INSTALLED_TEST_VERSION})`,
`Run ${UpdaterConfig.commands.selfUpdate} to install it.`,
].join("\n"),
);
Expand All @@ -803,7 +817,9 @@ describe("update command", () => {
});

test("upgrade aliases update", async () => {
const output = await runUpdateCommand(["upgrade", UPDATE_TEST_VERSION, "--check"]);
const output = await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
runUpdateCommand(["upgrade", UPDATE_TEST_VERSION, "--check"]),
);

expect(output.stdout[0]).toContain(
`A new version of altertable is available: v${UPDATE_TEST_VERSION}`,
Expand Down Expand Up @@ -831,7 +847,9 @@ describe("update command", () => {
},
};

await executeUpdateCommand({}, runtime.output, dependencies);
await withInstalledVersion(INSTALLED_TEST_VERSION, () =>
executeUpdateCommand({}, runtime.output, dependencies),
);

expect(installedVersion).toBe(UPDATE_TEST_VERSION);
expect(output.stderr).toContain(`Updating altertable to ${UPDATE_TEST_VERSION}`);
Expand Down
Loading