From 78233484250612bd6cd0851ac4a3ebe7adfa4627 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:10:09 +0000 Subject: [PATCH 1/2] fix(docker): bump the docker-minor group Bumps the docker-minor group in /apps/cli-go/pkg/config/templates with 3 updates: supabase/studio, supabase/realtime and supabase/storage-api. Updates `supabase/studio` from 2026.06.03-sha-0bca601 to 2026.06.08-sha-8af2bb0 Updates `supabase/realtime` from v2.103.4 to v2.105.0 Updates `supabase/storage-api` from v1.60.8 to v1.60.11 --- updated-dependencies: - dependency-name: supabase/studio dependency-version: 2026.06.08-sha-8af2bb0 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/realtime dependency-version: v2.105.0 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/storage-api dependency-version: v1.60.11 dependency-type: direct:production dependency-group: docker-minor ... Signed-off-by: dependabot[bot] --- apps/cli-go/pkg/config/templates/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 498f81796..447f7c40d 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -5,14 +5,14 @@ FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v14.13 AS postgrest FROM supabase/postgres-meta:v0.96.6 AS pgmeta -FROM supabase/studio:2026.06.03-sha-0bca601 AS studio +FROM supabase/studio:2026.06.08-sha-8af2bb0 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.74.0 AS edgeruntime FROM timberio/vector:0.53.0-alpine AS vector FROM supabase/supavisor:2.9.7 AS supavisor FROM supabase/gotrue:v2.189.0 AS gotrue -FROM supabase/realtime:v2.103.4 AS realtime -FROM supabase/storage-api:v1.60.8 AS storage +FROM supabase/realtime:v2.105.0 AS realtime +FROM supabase/storage-api:v1.60.11 AS storage FROM supabase/logflare:1.43.4 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ From 9329960e40bb83723b1f8e5912c86e8e9215dcf2 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 9 Jun 2026 11:33:32 +0200 Subject: [PATCH 2/2] fix(cli): derive services images from Dockerfile --- apps/cli-e2e/src/tests/stack.e2e.test.ts | 2 +- apps/cli-e2e/src/tests/test-context.ts | 2 + apps/cli/src/globals.d.ts | 5 + .../src/shared/services/services.shared.ts | 100 +++++++++++++----- .../services/services.shared.unit.test.ts | 51 +++++++++ apps/cli/vitest.config.ts | 19 ++++ packages/cli-test-helpers/src/normalize.ts | 30 ++++-- .../src/normalize.unit.test.ts | 6 ++ packages/cli-test-helpers/src/parity.ts | 11 +- 9 files changed, 186 insertions(+), 40 deletions(-) diff --git a/apps/cli-e2e/src/tests/stack.e2e.test.ts b/apps/cli-e2e/src/tests/stack.e2e.test.ts index c07d620b2..f86a1e474 100644 --- a/apps/cli-e2e/src/tests/stack.e2e.test.ts +++ b/apps/cli-e2e/src/tests/stack.e2e.test.ts @@ -73,7 +73,7 @@ describe("services", () => { expect(result.stdout).toContain("storage"); }); - testParity(["services"]); + testParity(["services"], { normalizeVersions: false }); }); // --------------------------------------------------------------------------- diff --git a/apps/cli-e2e/src/tests/test-context.ts b/apps/cli-e2e/src/tests/test-context.ts index 613360f34..1c0773fbe 100644 --- a/apps/cli-e2e/src/tests/test-context.ts +++ b/apps/cli-e2e/src/tests/test-context.ts @@ -145,6 +145,7 @@ export function testParity( failureType?: FailureType; workspaceSetup?: (dir: string) => void; sortStdoutRows?: boolean; + normalizeVersions?: boolean; }, ): void { const label = opts?.failureType @@ -169,6 +170,7 @@ export function testParity( accessToken: ACCESS_TOKEN, workspaceSetup: opts?.workspaceSetup, sortStdoutRows: opts?.sortStdoutRows, + normalize: { versions: opts?.normalizeVersions }, }, cmd, ); diff --git a/apps/cli/src/globals.d.ts b/apps/cli/src/globals.d.ts index c94d67b1a..ae39574f8 100644 --- a/apps/cli/src/globals.d.ts +++ b/apps/cli/src/globals.d.ts @@ -2,3 +2,8 @@ declare module "*.md" { const content: string; export default content; } + +declare module "*Dockerfile" { + const content: string; + export default content; +} diff --git a/apps/cli/src/shared/services/services.shared.ts b/apps/cli/src/shared/services/services.shared.ts index d4016d9a1..502485391 100644 --- a/apps/cli/src/shared/services/services.shared.ts +++ b/apps/cli/src/shared/services/services.shared.ts @@ -3,6 +3,7 @@ import { makeApiClient, type ApiClient } from "@supabase/api/effect"; import { Data, Duration, Effect, Exit, Redacted } from "effect"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import serviceImagesDockerfile from "../../../../cli-go/pkg/config/templates/Dockerfile" with { type: "text" }; import { renderGlamourTable } from "../../legacy/output/legacy-glamour-table.ts"; export type RemoteServiceName = "postgres" | "auth" | "postgrest" | "storage"; @@ -19,28 +20,73 @@ interface ServiceImageSpec { readonly remoteService: RemoteServiceName | undefined; } -// Mirrors the legacy `services` image matrix: -// - source versions: `apps/cli-go/pkg/config/templates/Dockerfile` -// - source order: `apps/cli-go/pkg/config/config.go` `GetServiceImages()` -// -// We keep this compiled into the TS CLI because the published package does not -// ship the Go source tree at runtime, but the user-visible `services` output -// still needs to match the bundled image manifest. -const LOCAL_SERVICE_IMAGES = [ - { image: "supabase/postgres:17.6.1.132", remoteService: "postgres" }, - { image: "supabase/gotrue:v2.189.0", remoteService: "auth" }, - { image: "postgrest/postgrest:v14.12", remoteService: "postgrest" }, - { image: "supabase/realtime:v2.103.2", remoteService: undefined }, - { image: "supabase/storage-api:v1.60.4", remoteService: "storage" }, - { image: "supabase/edge-runtime:v1.74.0", remoteService: undefined }, - { - image: "supabase/studio:2026.06.03-sha-0bca601", - remoteService: undefined, - }, - { image: "supabase/postgres-meta:v0.96.6", remoteService: undefined }, - { image: "supabase/logflare:1.43.3", remoteService: undefined }, - { image: "supabase/supavisor:2.9.7", remoteService: undefined }, -] as const satisfies ReadonlyArray; +interface DockerfileImageSpec { + readonly alias: string; + readonly image: string; +} + +interface ServiceImageAliasSpec { + readonly alias: string; + readonly remoteService: RemoteServiceName | undefined; +} + +const SERVICE_IMAGE_ALIASES: ReadonlyArray = [ + { alias: "pg", remoteService: "postgres" }, + { alias: "gotrue", remoteService: "auth" }, + { alias: "postgrest", remoteService: "postgrest" }, + { alias: "realtime", remoteService: undefined }, + { alias: "storage", remoteService: "storage" }, + { alias: "edgeruntime", remoteService: undefined }, + { alias: "studio", remoteService: undefined }, + { alias: "pgmeta", remoteService: undefined }, + { alias: "logflare", remoteService: undefined }, + { alias: "supavisor", remoteService: undefined }, +]; + +const FROM_LINE_PATTERN = /^FROM\s+(.+):([^:\s]+)\s+AS\s+([^\s#]+)/i; + +export function parseDockerfileServiceImages( + dockerfile: string, +): ReadonlyArray { + return dockerfile + .split("\n") + .map((line) => line.trim()) + .flatMap((line) => { + const match = FROM_LINE_PATTERN.exec(line); + if (match === null) { + return []; + } + + const [, repository, tag, alias] = match; + if (repository === undefined || tag === undefined || alias === undefined) { + return []; + } + + return [{ alias, image: `${repository}:${tag}` }]; + }); +} + +export function localServiceImagesFromDockerfile( + dockerfile: string, +): ReadonlyArray { + const imagesByAlias = new Map( + parseDockerfileServiceImages(dockerfile).map((service) => [service.alias, service.image]), + ); + + return SERVICE_IMAGE_ALIASES.map((service) => { + const image = imagesByAlias.get(service.alias); + if (image === undefined) { + throw new Error(`Missing service image alias '${service.alias}' in Dockerfile manifest.`); + } + + return { + image, + remoteService: service.remoteService, + }; + }); +} + +const LOCAL_SERVICE_IMAGES = localServiceImagesFromDockerfile(serviceImagesDockerfile); const TABLE_HEADERS = ["SERVICE IMAGE", "LOCAL", "LINKED"] as const; @@ -61,14 +107,14 @@ function toServiceVersionRow( service: ServiceImageSpec, remote: Partial> = {}, ): ServiceVersionRow { - const parts = service.image.split(":"); - const name = parts[0]; - const local = parts[1]; - - if (name === undefined || local === undefined) { + const tagSeparator = service.image.lastIndexOf(":"); + if (tagSeparator === -1) { throw new Error(`Invalid service image entry: ${service.image}`); } + const name = service.image.slice(0, tagSeparator); + const local = service.image.slice(tagSeparator + 1); + return { name, local, diff --git a/apps/cli/src/shared/services/services.shared.unit.test.ts b/apps/cli/src/shared/services/services.shared.unit.test.ts index 3b38f80e7..27be16171 100644 --- a/apps/cli/src/shared/services/services.shared.unit.test.ts +++ b/apps/cli/src/shared/services/services.shared.unit.test.ts @@ -1,9 +1,12 @@ import { describe, expect, test } from "vitest"; import { Effect, Redacted } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; +import serviceImagesDockerfile from "../../../../cli-go/pkg/config/templates/Dockerfile" with { type: "text" }; import { fetchLinkedServiceVersions, listLocalServiceVersions, + localServiceImagesFromDockerfile, + parseDockerfileServiceImages, renderServicesTable, renderServicesWarning, } from "./services.shared.ts"; @@ -17,6 +20,54 @@ const runLinkedFetch = (input: Parameters[0]) Effect.runPromise(fetchLinkedServiceVersions(input).pipe(Effect.provide(FetchHttpClient.layer))); describe("services shared", () => { + test("parses service images from Dockerfile FROM aliases", () => { + expect( + parseDockerfileServiceImages(` + # comment + FROM supabase/postgres:17.6.1.132 AS pg + + RUN echo ignored + FROM localhost:5000/custom/image:1.2.3 AS custom + `), + ).toEqual([ + { alias: "pg", image: "supabase/postgres:17.6.1.132" }, + { alias: "custom", image: "localhost:5000/custom/image:1.2.3" }, + ]); + }); + + test("fails clearly when the Dockerfile manifest misses a required service alias", () => { + expect(() => + localServiceImagesFromDockerfile("FROM supabase/postgres:17.6.1.132 AS pg\n"), + ).toThrow("Missing service image alias 'gotrue' in Dockerfile manifest."); + }); + + test("derives local service versions from the Go Dockerfile manifest", () => { + const rows = listLocalServiceVersions(); + const dockerfileImages = localServiceImagesFromDockerfile(serviceImagesDockerfile); + const expectedRows = dockerfileImages.map((service) => { + const tagSeparator = service.image.lastIndexOf(":"); + return { + name: service.image.slice(0, tagSeparator), + local: service.image.slice(tagSeparator + 1), + remote: "", + }; + }); + + expect(rows).toEqual(expectedRows); + expect(rows.map((row) => row.name)).toEqual([ + "supabase/postgres", + "supabase/gotrue", + "postgrest/postgrest", + "supabase/realtime", + "supabase/storage-api", + "supabase/edge-runtime", + "supabase/studio", + "supabase/postgres-meta", + "supabase/logflare", + "supabase/supavisor", + ]); + }); + test("returns postgres only when no service-role key is available", async () => { const server = Bun.serve({ port: 0, diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts index d6e479778..412bff1fc 100644 --- a/apps/cli/vitest.config.ts +++ b/apps/cli/vitest.config.ts @@ -1,6 +1,22 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; +function dockerfileTextPlugin() { + return { + name: "dockerfile-text-loader", + load(id: string) { + const [filePath] = id.split("?", 2); + if (filePath?.endsWith("/Dockerfile") !== true) { + return undefined; + } + + return `export default ${JSON.stringify(readFileSync(filePath, "utf8"))};`; + }, + }; +} + export default defineConfig({ + plugins: [dockerfileTextPlugin()], test: { passWithNoTests: true, coverage: { @@ -24,18 +40,21 @@ export default defineConfig({ }, projects: [ { + plugins: [dockerfileTextPlugin()], test: { name: "unit", include: ["**/*.unit.test.ts"], }, }, { + plugins: [dockerfileTextPlugin()], test: { name: "integration", include: ["**/*.integration.test.ts"], }, }, { + plugins: [dockerfileTextPlugin()], test: { name: "e2e", include: ["**/*.e2e.test.ts"], diff --git a/packages/cli-test-helpers/src/normalize.ts b/packages/cli-test-helpers/src/normalize.ts index ddcf0ad7a..3fa62df51 100644 --- a/packages/cli-test-helpers/src/normalize.ts +++ b/packages/cli-test-helpers/src/normalize.ts @@ -29,21 +29,33 @@ export function sortTableRows(output: string): string { return result.join("\n"); } +export interface NormalizeOptions { + readonly versions?: boolean; +} + /** * Normalize CLI output by stripping non-deterministic content before parity * comparisons. Applied to both Go and ts-legacy output so spurious differences * in timestamps, versions, paths, and stack traces don't produce false failures. */ -export function normalize(output: string): string { +export function normalize(output: string, options: NormalizeOptions = {}): string { + const normalizeVersions = options.versions ?? true; + const withoutAnsi = output + // 1. Strip ANSI escape codes (color, bold, reset, etc.) — \u001b is ESC + // eslint-disable-next-line no-control-regex + .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, ""); + const withoutVersions = normalizeVersions + ? withoutAnsi.replace( + // 2. Semantic version strings (e.g. 1.187.0, v2.0.0-rc.1, v14.13). + // Lookbehind prevents matching mid-IP-address (e.g. 0.0.1 inside 127.0.0.1). + // Lookahead prevents matching where more dotted-number segments follow. + /(?", + ) + : withoutAnsi; + return ( - output - // 1. Strip ANSI escape codes (color, bold, reset, etc.) — \u001b is ESC - // eslint-disable-next-line no-control-regex - .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, "") - // 2. Semantic version strings (e.g. 1.187.0, v2.0.0-rc.1, v14.13). - // Lookbehind prevents matching mid-IP-address (e.g. 0.0.1 inside 127.0.0.1). - // Lookahead prevents matching where more dotted-number segments follow. - .replace(/(?") + withoutVersions // 3. ISO-8601 timestamps (2026-04-15T10:46:15Z or with milliseconds) .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "") // 4. Display timestamps (2026-04-15 10:46:15 — space-separated, no T) diff --git a/packages/cli-test-helpers/src/normalize.unit.test.ts b/packages/cli-test-helpers/src/normalize.unit.test.ts index a67c486e5..87881eb7b 100644 --- a/packages/cli-test-helpers/src/normalize.unit.test.ts +++ b/packages/cli-test-helpers/src/normalize.unit.test.ts @@ -15,6 +15,12 @@ describe("normalize", () => { expect(normalize("Version: 0.1.0-rc.1")).toBe("Version: "); }); + it("can preserve semantic version strings", () => { + expect(normalize("postgrest/postgrest:v14.13", { versions: false })).toBe( + "postgrest/postgrest:v14.13", + ); + }); + it("does not normalize IP addresses as version strings", () => { expect(normalize("host 127.0.0.1")).toBe("host 127.0.0.1"); expect(normalize("192.168.1.1")).toBe("192.168.1.1"); diff --git a/packages/cli-test-helpers/src/parity.ts b/packages/cli-test-helpers/src/parity.ts index b99f1f06b..8c2e6ef8f 100644 --- a/packages/cli-test-helpers/src/parity.ts +++ b/packages/cli-test-helpers/src/parity.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { createHarness, exec, makeTempDir } from "./harness.ts"; -import { normalize, sortTableRows } from "./normalize.ts"; +import { normalize, type NormalizeOptions, sortTableRows } from "./normalize.ts"; // --------------------------------------------------------------------------- // Table parsing (Level 2) @@ -215,13 +215,14 @@ async function collectRunResult( dir: string, apiUrl: string, extraEnv?: Record, + normalizeOptions?: NormalizeOptions, ): Promise { const result = await exec(harness, cmd, extraEnv ? { env: extraEnv } : undefined); const requests = await fetchRequestLog(apiUrl); const files = snapshotChangedFiles(dir); return { - stdout: normalize(result.stdout), - stderr: normalize(result.stderr), + stdout: normalize(result.stdout, normalizeOptions), + stderr: normalize(result.stderr, normalizeOptions), exitCode: result.exitCode, requests, files, @@ -311,6 +312,8 @@ export interface ParityOptions { sortStdoutRows?: boolean; /** Additional environment variables injected into both CLI subprocesses. */ extraEnv?: Record; + /** Fine-grained normalization controls for stdout/stderr parity comparison. */ + normalize?: NormalizeOptions; } /** @@ -341,6 +344,7 @@ export async function runParity(opts: ParityOptions, cmd: string[]): Promise