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
99 changes: 97 additions & 2 deletions apps/cli/src/legacy/commands/services/services.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { LegacyCliConfig } from "../../config/legacy-cli-config.service.ts";
import { LegacyCredentials } from "../../auth/legacy-credentials.service.ts";
import { LegacyLinkedProjectCache } from "../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../telemetry/legacy-telemetry-state.service.ts";
import { legacyReadDbToml } from "../../shared/legacy-db-config.toml-read.ts";
import { legacyResolveDbImage } from "../../shared/legacy-db-image.ts";
import { legacyResolveEdgeRuntimeImage } from "../../shared/legacy-edge-runtime-image.ts";
import { legacyTempPaths } from "../../shared/legacy-temp-paths.ts";
import { LegacyOutputFlag } from "../../../shared/legacy/global-flags.ts";
import { Output } from "../../../shared/output/output.service.ts";
import { encodeGoJson, encodeToml, encodeYaml } from "../../shared/legacy-go-output.encoders.ts";
Expand All @@ -11,6 +15,9 @@ import {
fetchLinkedServiceVersions,
formatServicesWarning,
listLocalServiceVersions,
type LocalServiceImageOverrides,
type LocalServiceVersionName,
type LocalServiceVersionOverrides,
mergeRemoteServiceVersions,
renderServicesTable,
renderServicesWarning,
Expand Down Expand Up @@ -55,8 +62,53 @@ export const legacyServices = Effect.fn("legacy.services")(function* (_flags: Le
yield* Effect.gen(function* () {
const accessTokenExit = yield* credentials.getAccessToken.pipe(Effect.exit);
const accessToken = Exit.isSuccess(accessTokenExit) ? accessTokenExit.value : Option.none();
const tomlValues = yield* legacyReadDbToml(
fs,
path,
cliConfig.workdir,
Option.getOrUndefined(linkedProjectRef),
).pipe(
Effect.catch((error) =>
output.raw(`${formatConfigLoadError(error)}\n`, "stderr").pipe(Effect.as(null)),
),
);
const serviceVersions =
tomlValues === null
? {}
: yield* readLegacyServiceVersionOverrides(
fs,
path,
cliConfig.workdir,
tomlValues.majorVersion,
);
const postgresImage =
tomlValues === null
? undefined
: yield* legacyResolveDbImage(
fs,
path,
cliConfig.workdir,
tomlValues.majorVersion,
Option.getOrUndefined(tomlValues.orioledbVersion),
);
const edgeRuntimeImage =
tomlValues === null
? undefined
: yield* legacyResolveEdgeRuntimeImage(fs, path, cliConfig.workdir, tomlValues.denoVersion);
const imageOverrides: LocalServiceImageOverrides = {};
if (postgresImage !== undefined) {
imageOverrides.postgres = postgresImage;
}
if (edgeRuntimeImage !== undefined) {
imageOverrides["edge-runtime"] = edgeRuntimeImage;
}
const localImageOptions = {
imageOverrides,
normalizeVersionTags: false,
serviceVersions,
};

let rows = listLocalServiceVersions();
let rows = listLocalServiceVersions(localImageOptions);
if (Option.isSome(linkedProjectRef) && Option.isSome(accessToken)) {
const remote = yield* fetchLinkedServiceVersions({
apiUrl: cliConfig.apiUrl,
Expand All @@ -65,7 +117,7 @@ export const legacyServices = Effect.fn("legacy.services")(function* (_flags: Le
accessToken: accessToken.value,
userAgent: cliConfig.userAgent,
});
rows = mergeRemoteServiceVersions(remote);
rows = mergeRemoteServiceVersions(remote, localImageOptions);
}

const warning = renderServicesWarning(rows);
Expand Down Expand Up @@ -110,3 +162,46 @@ export const legacyServices = Effect.fn("legacy.services")(function* (_flags: Le
yield* output.raw(renderServicesTable(rows));
}).pipe(Effect.ensuring(cacheLinkedProject), Effect.ensuring(telemetryState.flush));
});

function formatConfigLoadError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

const LEGACY_VERSION_FILES = [
["auth", "gotrue-version", (majorVersion: number | undefined) => (majorVersion ?? 17) > 14],
["postgrest", "rest-version", (majorVersion: number | undefined) => (majorVersion ?? 17) > 14],
["storage", "storage-version"],
["realtime", "realtime-version"],
["studio", "studio-version"],
["pgmeta", "pgmeta-version"],
["analytics", "logflare-version"],
["pooler", "pooler-version"],
] as const satisfies ReadonlyArray<
readonly [LocalServiceVersionName, string, ((majorVersion: number | undefined) => boolean)?]
>;

const readLegacyServiceVersionOverrides = Effect.fnUntraced(function* (
fs: FileSystem.FileSystem,
path: Path.Path,
workdir: string,
majorVersion: number | undefined,
) {
const paths = legacyTempPaths(path, workdir);
const versions: LocalServiceVersionOverrides = {};

for (const [service, fileName, shouldRead] of LEGACY_VERSION_FILES) {
if (shouldRead !== undefined && !shouldRead(majorVersion)) {
continue;
}

const version = yield* fs.readFileString(path.join(paths.tempDir, fileName)).pipe(
Effect.map((content) => content.trim()),
Effect.orElseSucceed(() => ""),
);
if (version.length > 0) {
versions[service] = version;
}
}

return versions;
});
179 changes: 176 additions & 3 deletions apps/cli/src/legacy/commands/services/services.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtempSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@effect/vitest";
Expand All @@ -19,7 +19,10 @@ import {
processEnvLayer,
} from "../../../../tests/helpers/mocks.ts";
import { mockLegacyTelemetryStateTracked } from "../../../../tests/helpers/legacy-mocks.ts";
import { listLocalServiceVersions } from "../../../shared/services/services.shared.ts";
import {
listLocalServiceVersions,
postgresImageForDbMajorVersion,
} from "../../../shared/services/services.shared.ts";
import { textCliOutputFormatter } from "../../../shared/output/text-formatter.ts";
import { processControlLayer } from "../../../shared/runtime/process-control.layer.ts";
import { TelemetryRuntime } from "../../../shared/telemetry/runtime.service.ts";
Expand All @@ -41,6 +44,7 @@ function setup(
opts: {
format?: "text" | "json" | "stream-json";
goOutput?: Option.Option<"env" | "pretty" | "json" | "toml" | "yaml">;
workdir?: string;
} = {},
) {
const out = mockOutput({
Expand Down Expand Up @@ -69,7 +73,7 @@ function setup(
poolerHost: "supabase.com",
accessToken: Option.none(),
projectId: Option.none(),
workdir: process.cwd(),
workdir: opts.workdir ?? process.cwd(),
userAgent: "SupabaseCLI/test",
}),
),
Expand Down Expand Up @@ -100,6 +104,38 @@ const legacyTestRoot = Command.make("supabase").pipe(
Command.withSubcommands([legacyServicesCommand]),
);

function makeProjectWithConfig(config: string): string {
const workdir = mkdtempSync(join(tmpdir(), "supabase-services-config-"));
const configDir = join(workdir, "supabase");
mkdirSync(configDir, { recursive: true });
writeFileSync(join(configDir, "config.toml"), config);
return workdir;
}

function makeProjectWithConfigFiles(opts: { toml: string; json: string }): string {
const workdir = makeProjectWithConfig(opts.toml);
writeFileSync(join(workdir, "supabase", "config.json"), opts.json);
return workdir;
}

function makeProjectWithDbMajorVersion(majorVersion: number): string {
return makeProjectWithConfig(`[db]\nmajor_version = ${majorVersion}\n`);
}

function writeTempFile(workdir: string, name: string, content: string): void {
const tempDir = join(workdir, "supabase", ".temp");
mkdirSync(tempDir, { recursive: true });
writeFileSync(join(tempDir, name), content);
}

function postgresVersionForDbMajorVersion(majorVersion: number): string {
const image = postgresImageForDbMajorVersion(majorVersion);
if (image === undefined) {
throw new Error(`Missing Postgres image for db major ${majorVersion}.`);
}
return image.slice(image.lastIndexOf(":") + 1);
}

function expectFailureTag(exit: Exit.Exit<unknown, unknown>, tag: string) {
expect(Exit.isFailure(exit)).toBe(true);
if (!Exit.isFailure(exit)) {
Expand Down Expand Up @@ -197,6 +233,143 @@ describe("legacy services", () => {
});
});

it.live("reports the configured Postgres version for local projects", () => {
const workdir = makeProjectWithDbMajorVersion(15);
const { layer, out } = setup({ goOutput: Option.some("json"), workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

const rows = JSON.parse(out.stdoutText) as Array<{
name: string;
local: string;
remote: string;
}>;
expect(rows).toContainEqual(
expect.objectContaining({
name: "supabase/postgres",
local: postgresVersionForDbMajorVersion(15),
}),
);
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("ignores config.json and reads legacy config.toml for local image selection", () => {
const workdir = makeProjectWithConfigFiles({
toml: "[db]\nmajor_version = 15\n",
json: JSON.stringify({ db: { major_version: 14 } }),
});
const { layer, out } = setup({ goOutput: Option.some("json"), workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

const rows = JSON.parse(out.stdoutText) as Array<{
name: string;
local: string;
remote: string;
}>;
expect(rows).toContainEqual(
expect.objectContaining({
name: "supabase/postgres",
local: postgresVersionForDbMajorVersion(15),
}),
);
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("applies linked-project remote config overrides when choosing the local image", () => {
const workdir = makeProjectWithConfig(`
[db]
major_version = 17

[remotes.linked]
project_id = "abcdefghijklmnopqrst"

[remotes.linked.db]
major_version = 15
`);
writeTempFile(workdir, "project-ref", "abcdefghijklmnopqrst");
const { layer, out } = setup({ goOutput: Option.some("json"), workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

const rows = JSON.parse(out.stdoutText) as Array<{
name: string;
local: string;
remote: string;
}>;
expect(rows).toContainEqual(
expect.objectContaining({
name: "supabase/postgres",
local: postgresVersionForDbMajorVersion(15),
}),
);
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("reports pinned legacy temp service versions", () => {
const workdir = makeProjectWithDbMajorVersion(15);
writeTempFile(workdir, "postgres-version", "15.1.0.117\n");
writeTempFile(workdir, "gotrue-version", "2.74.2\n");
writeTempFile(workdir, "storage-version", "v1.28.0\n");
const { layer, out } = setup({ goOutput: Option.some("json"), workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

const rows = JSON.parse(out.stdoutText) as Array<{
name: string;
local: string;
remote: string;
}>;
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "supabase/postgres", local: "15.1.0.117" }),
expect.objectContaining({ name: "supabase/gotrue", local: "2.74.2" }),
expect.objectContaining({ name: "supabase/storage-api", local: "v1.28.0" }),
]),
);
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("reports the Deno 1 edge-runtime image instead of the temp pin", () => {
const workdir = makeProjectWithConfig("[edge_runtime]\ndeno_version = 1\n");
writeTempFile(workdir, "edge-runtime-version", "v9.9.9\n");
const { layer, out } = setup({ goOutput: Option.some("json"), workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

const rows = JSON.parse(out.stdoutText) as Array<{
name: string;
local: string;
remote: string;
}>;
expect(rows).toContainEqual(
expect.objectContaining({
name: "supabase/edge-runtime",
local: "v1.68.4",
}),
);
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("prints config load errors and falls back to the default matrix", () => {
const workdir = makeProjectWithConfig("[db]\nmajor_version = ");
writeTempFile(workdir, "storage-version", "v9.9.9\n");
const { layer, out } = setup({ workdir });

return Effect.gen(function* () {
yield* legacyServices({}).pipe(Effect.provide(layer));

expect(out.stdoutText).toContain("supabase/postgres");
expect(out.stdoutText).not.toContain("v9.9.9");
expect(out.stderrText).not.toBe("");
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))));
});

it.live("emits structured JSON for --output pretty combined with --output-format json", () => {
// Regression guard (CLI-1546): a Go `--output pretty` must defer to the TS
// `--output-format json` flag instead of forcing the human-readable table.
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const DEFAULT_PORT = 54322;
const DEFAULT_SHADOW_PORT = 54320;
const DEFAULT_MAJOR_VERSION = 17;
const DEFAULT_PASSWORD = "postgres";
/** `[edge_runtime] deno_version` default (`config.toml` template). 2 → v1.74.1. */
/** `[edge_runtime] deno_version` default (`config.toml` template). 2 → the current edge-runtime image. */
const DEFAULT_DENO_VERSION = 2;

/** Default declarative schema dir (`utils.DeclarativeDir`, `misc.go:102`). */
Expand Down
10 changes: 5 additions & 5 deletions apps/cli/src/legacy/shared/legacy-edge-runtime-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { Effect, type FileSystem, type Path } from "effect";
* declarative pg-delta scripts that run inside the edge-runtime container.
*
* The default tag is baked into the Go binary via the embedded Dockerfile
* (`FROM supabase/edge-runtime:v1.74.1 AS edgeruntime`), mirrored here as a
* (`FROM supabase/edge-runtime:v1.74.2 AS edgeruntime`), mirrored here as a
* constant. A pinned tag in `supabase/.temp/edge-runtime-version` overrides it
* (written by `supabase start`). `edge_runtime.deno_version = 1` selects the
* legacy `deno1` image instead (default `deno_version = 2` keeps v1.74.1).
* legacy `deno1` image instead (default `deno_version = 2` keeps v1.74.2).
*/

// `FROM supabase/edge-runtime:v1.74.1 AS edgeruntime` (embedded Dockerfile).
export const LEGACY_EDGE_RUNTIME_IMAGE = "supabase/edge-runtime:v1.74.1";
// `FROM supabase/edge-runtime:v1.74.2 AS edgeruntime` (embedded Dockerfile).
export const LEGACY_EDGE_RUNTIME_IMAGE = "supabase/edge-runtime:v1.74.2";
// `deno1` (`pkg/config/constants.go:15`) — used when `deno_version = 1`.
const LEGACY_EDGE_RUNTIME_DENO1_IMAGE = "supabase/edge-runtime:v1.68.4";

Expand All @@ -26,7 +26,7 @@ function replaceImageTag(image: string, tag: string): string {
/**
* Resolve the edge-runtime image, honoring the pinned tag in
* `supabase/.temp/edge-runtime-version` and the `deno_version` selector
* (default 2 → v1.74.1; 1 → `deno1`). The version pin is applied first (Go's
* (default 2 → v1.74.2; 1 → `deno1`). The version pin is applied first (Go's
* `Load`), then `deno_version = 1` overrides to `deno1` (Go's validate pass).
*/
export const legacyResolveEdgeRuntimeImage = Effect.fnUntraced(function* (
Expand Down
Loading
Loading