From 69dd4a5fa34990ae5a6079debcd3bb0b7e6c4b98 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Fri, 26 Jun 2026 18:02:15 +0200 Subject: [PATCH 01/12] chore(stack): sync service versions from Dockerfile --- .../workflows/sync-stack-service-versions.yml | 59 ++++++++ packages/stack/package.json | 2 + .../scripts/sync-versions-from-dockerfile.ts | 132 ++++++++++++++++++ .../stack/src/BinaryResolver.unit.test.ts | 5 +- packages/stack/src/StackBuilder.unit.test.ts | 6 +- packages/stack/src/prefetch.unit.test.ts | 23 ++- packages/stack/src/versions.ts | 22 +-- packages/stack/src/versions.unit.test.ts | 87 ++++++++++++ 8 files changed, 309 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/sync-stack-service-versions.yml create mode 100644 packages/stack/scripts/sync-versions-from-dockerfile.ts diff --git a/.github/workflows/sync-stack-service-versions.yml b/.github/workflows/sync-stack-service-versions.yml new file mode 100644 index 0000000000..95f15ef4c5 --- /dev/null +++ b/.github/workflows/sync-stack-service-versions.yml @@ -0,0 +1,59 @@ +name: Sync Stack Service Versions + +on: + pull_request: + types: + - opened + - synchronize + - reopened + paths: + - apps/cli-go/pkg/config/templates/Dockerfile + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + sync: + name: Sync stack service versions + runs-on: blacksmith-2vcpu-ubuntu-2404 + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name + steps: + - name: Generate token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.GH_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + permission-contents: write + + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Setup + uses: ./.github/actions/setup + with: + dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + + - name: Sync stack service versions + run: pnpm sync:versions + working-directory: packages/stack + + - name: Commit synced stack service versions + run: | + if git diff --quiet -- packages/stack/src/versions.ts; then + echo "Stack service versions are already synced." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add packages/stack/src/versions.ts + git commit -m "chore(stack): sync service version manifest" + git push diff --git a/packages/stack/package.json b/packages/stack/package.json index 1c97b1c98a..6795a55c34 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -15,6 +15,7 @@ "test": "nx run-many -t test:core test:e2e --projects=$npm_package_name", "test:core": "nx run-many -t test:unit test:integration --projects=$npm_package_name", "test:e2e:warmup": "bun run tests/warmup-e2e.ts", + "sync:versions": "bun run scripts/sync-versions-from-dockerfile.ts", "check:all": "nx run-many -t types:check lint:check fmt:check knip:check --projects=$npm_package_name", "fix:all": "nx run-many -t lint:fix fmt:fix knip:fix --projects=$npm_package_name" }, @@ -40,6 +41,7 @@ }, "knip": { "entry": [ + "scripts/**/*.ts", "src/**/*.test.ts", "src/daemon-node.ts", "tests/**/*.ts" diff --git a/packages/stack/scripts/sync-versions-from-dockerfile.ts b/packages/stack/scripts/sync-versions-from-dockerfile.ts new file mode 100644 index 0000000000..39e4592ea2 --- /dev/null +++ b/packages/stack/scripts/sync-versions-from-dockerfile.ts @@ -0,0 +1,132 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + normalizeServiceVersion, + SERVICE_NAMES, + type ServiceName, + type VersionManifest, +} from "../src/versions.ts"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, "../../.."); +const dockerfilePath = path.join(repoRoot, "apps/cli-go/pkg/config/templates/Dockerfile"); +const versionsPath = path.join(repoRoot, "packages/stack/src/versions.ts"); + +const fromLinePattern = /^FROM\s+(.+):([^:\s]+)\s+AS\s+([^\s#]+)/i; + +const dockerfileAliases = new Map([ + ["pg", "postgres"], + ["postgrest", "postgrest"], + ["gotrue", "auth"], + ["edgeruntime", "edge-runtime"], + ["realtime", "realtime"], + ["storage", "storage"], + ["imgproxy", "imgproxy"], + ["mailpit", "mailpit"], + ["pgmeta", "pgmeta"], + ["studio", "studio"], + ["logflare", "analytics"], + ["vector", "vector"], + ["supavisor", "pooler"], +]); + +const ignoredAliases = new Set(["kong", "differ", "migra", "pgprove"]); + +function assertFullManifest( + versions: Partial>, +): asserts versions is VersionManifest { + const missing = SERVICE_NAMES.filter((service) => versions[service] === undefined); + if (missing.length > 0) { + throw new Error(`Missing Dockerfile versions for: ${missing.join(", ")}`); + } +} + +export function readVersionManifestFromDockerfile(dockerfile: string): VersionManifest { + const versions: Partial> = {}; + + for (const rawLine of dockerfile.split("\n")) { + const line = rawLine.trim(); + const match = fromLinePattern.exec(line); + if (match === null) { + continue; + } + + const [, , tag, alias] = match; + if (tag === undefined || alias === undefined) { + continue; + } + + if (ignoredAliases.has(alias)) { + continue; + } + + const service = dockerfileAliases.get(alias); + if (service === undefined) { + throw new Error(`Unknown Dockerfile image alias '${alias}'.`); + } + if (versions[service] !== undefined) { + throw new Error(`Duplicate Dockerfile version for '${service}'.`); + } + + versions[service] = normalizeServiceVersion(service, tag); + } + + assertFullManifest(versions); + return versions; +} + +function renderManifestKey(service: ServiceName): string { + return /^[a-zA-Z_$][\w$]*$/.test(service) ? service : JSON.stringify(service); +} + +export function renderDefaultVersions(versions: VersionManifest): string { + const lines = SERVICE_NAMES.map( + (service) => ` ${renderManifestKey(service)}: ${JSON.stringify(versions[service])},`, + ); + return ["export const DEFAULT_VERSIONS: VersionManifest = {", ...lines, "} as const;"].join("\n"); +} + +export function syncDefaultVersionsSource(source: string, versions: VersionManifest): string { + const startMarker = "export const DEFAULT_VERSIONS: VersionManifest = {"; + const endMarker = "\n} as const;"; + const start = source.indexOf(startMarker); + if (start === -1) { + throw new Error("Could not find DEFAULT_VERSIONS declaration."); + } + + const end = source.indexOf(endMarker, start); + if (end === -1) { + throw new Error("Could not find DEFAULT_VERSIONS declaration end."); + } + + return `${source.slice(0, start)}${renderDefaultVersions(versions)}${source.slice( + end + endMarker.length, + )}`; +} + +async function main() { + const checkOnly = process.argv.includes("--check"); + const dockerfile = await readFile(dockerfilePath, "utf8"); + const versionsSource = await readFile(versionsPath, "utf8"); + const versions = readVersionManifestFromDockerfile(dockerfile); + const syncedSource = syncDefaultVersionsSource(versionsSource, versions); + + if (syncedSource === versionsSource) { + console.log("DEFAULT_VERSIONS is already synced with the Dockerfile manifest."); + return; + } + + if (checkOnly) { + console.error("DEFAULT_VERSIONS is out of sync with the Dockerfile manifest."); + process.exitCode = 1; + return; + } + + await Bun.write(versionsPath, syncedSource); + console.log("Synced DEFAULT_VERSIONS with the Dockerfile manifest."); +} + +if (import.meta.main) { + await main(); +} diff --git a/packages/stack/src/BinaryResolver.unit.test.ts b/packages/stack/src/BinaryResolver.unit.test.ts index bc09b3b901..b0d63ccc77 100644 --- a/packages/stack/src/BinaryResolver.unit.test.ts +++ b/packages/stack/src/BinaryResolver.unit.test.ts @@ -5,6 +5,7 @@ import { DEFAULT_VERSIONS } from "./versions.ts"; const postgresVersion = DEFAULT_VERSIONS.postgres; const postgrestVersion = DEFAULT_VERSIONS.postgrest; const authVersion = DEFAULT_VERSIONS.auth; +const authRcVersion = "2.188.0-rc.15"; const edgeRuntimeVersion = DEFAULT_VERSIONS["edge-runtime"]; describe("BinaryResolver.downloadUrl", () => { @@ -44,11 +45,11 @@ describe("BinaryResolver.downloadUrl", () => { it("constructs auth URL for rc releases", () => { const url = BinaryResolver.downloadUrl({ service: "auth", - version: authVersion, + version: authRcVersion, assetName: "arm64", }); expect(url).toBe( - `https://github.com/supabase/auth/releases/download/rc${authVersion}/auth-v${authVersion}-arm64.tar.gz`, + `https://github.com/supabase/auth/releases/download/rc${authRcVersion}/auth-v${authRcVersion}-arm64.tar.gz`, ); }); diff --git a/packages/stack/src/StackBuilder.unit.test.ts b/packages/stack/src/StackBuilder.unit.test.ts index 3d3c2c0039..261ccd277c 100644 --- a/packages/stack/src/StackBuilder.unit.test.ts +++ b/packages/stack/src/StackBuilder.unit.test.ts @@ -456,8 +456,10 @@ describe("StackBuilder", () => { }); const realtimeDef = graph.startOrder.find((service) => service.name === "realtime"); - expect(realtimeDef?.args).toContain("supabase/realtime:v2.111.8"); - expect(realtimeDef?.args).not.toContain("public.ecr.aws/supabase/realtime:v2.111.8"); + expect(realtimeDef?.args).toContain(`supabase/realtime:v${DEFAULT_VERSIONS.realtime}`); + expect(realtimeDef?.args).not.toContain( + `public.ecr.aws/supabase/realtime:v${DEFAULT_VERSIONS.realtime}`, + ); }).pipe(Effect.provide(layer)); }); }); diff --git a/packages/stack/src/prefetch.unit.test.ts b/packages/stack/src/prefetch.unit.test.ts index bf0e66d50a..1cf509d074 100644 --- a/packages/stack/src/prefetch.unit.test.ts +++ b/packages/stack/src/prefetch.unit.test.ts @@ -14,6 +14,9 @@ import { prepareAssetsWithDependencies } from "./StackPreparation.ts"; import { DEFAULT_VERSIONS, SERVICE_NAMES } from "./versions.ts"; const encoder = new TextEncoder(); +const defaultAuthEcrImage = `public.ecr.aws/supabase/gotrue:v${DEFAULT_VERSIONS.auth}`; +const defaultAuthDockerHubImage = `supabase/gotrue:v${DEFAULT_VERSIONS.auth}`; +const defaultAuthGhcrImage = `ghcr.io/supabase/gotrue:v${DEFAULT_VERSIONS.auth}`; interface SpawnResult { readonly exitCode: number; @@ -112,15 +115,11 @@ describe("prefetch", () => { expect(result.auth).toEqual({ type: "docker", - image: "supabase/gotrue:v2.188.0-rc.15", + image: defaultAuthDockerHubImage, }); expect( spawner.spawned.filter((record) => record.args[0] === "pull").map((record) => record.args[1]), - ).toEqual([ - "public.ecr.aws/supabase/gotrue:v2.188.0-rc.15", - "public.ecr.aws/supabase/gotrue:v2.188.0-rc.15", - "supabase/gotrue:v2.188.0-rc.15", - ]); + ).toEqual([defaultAuthEcrImage, defaultAuthEcrImage, defaultAuthDockerHubImage]); }); test("falls back to GHCR after ECR and Docker Hub fail", async () => { @@ -149,15 +148,15 @@ describe("prefetch", () => { expect(result.auth).toEqual({ type: "docker", - image: "ghcr.io/supabase/gotrue:v2.188.0-rc.15", + image: defaultAuthGhcrImage, }); expect( spawner.spawned.filter((record) => record.args[0] === "pull").map((record) => record.args[1]), ).toEqual([ - "public.ecr.aws/supabase/gotrue:v2.188.0-rc.15", - "supabase/gotrue:v2.188.0-rc.15", - "supabase/gotrue:v2.188.0-rc.15", - "ghcr.io/supabase/gotrue:v2.188.0-rc.15", + defaultAuthEcrImage, + defaultAuthDockerHubImage, + defaultAuthDockerHubImage, + defaultAuthGhcrImage, ]); }); @@ -224,7 +223,7 @@ describe("prefetch", () => { expect(result.auth).toEqual({ type: "docker", - image: "public.ecr.aws/supabase/gotrue:v2.188.0-rc.15", + image: defaultAuthEcrImage, }); expect(events).toEqual([]); }); diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index 273e47ffcc..6f784e874c 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -46,19 +46,19 @@ export interface VersionManifest { } export const DEFAULT_VERSIONS: VersionManifest = { - postgres: "17.6.1.107", - postgrest: "14.5", - auth: "2.188.0-rc.15", - "edge-runtime": "1.73.13", - realtime: "2.111.8", - storage: "1.41.8", + postgres: "17.6.1.139", + postgrest: "14.13", + auth: "2.191.0", + "edge-runtime": "1.74.2", + realtime: "2.111.10", + storage: "1.61.4", imgproxy: "v3.8.0", mailpit: "v1.30.2", - pgmeta: "0.96.1", - studio: "2026.03.04-sha-0043607", - analytics: "1.34.7", - vector: "0.28.1-alpine", - pooler: "2.7.4", + pgmeta: "0.96.6", + studio: "2026.06.22-sha-2207d7f", + analytics: "1.45.4", + vector: "0.53.0-alpine", + pooler: "2.9.7", } as const; /** Default registry. Matches the Go CLI default (`public.ecr.aws`). */ diff --git a/packages/stack/src/versions.unit.test.ts b/packages/stack/src/versions.unit.test.ts index 8f6141035f..a8440db663 100644 --- a/packages/stack/src/versions.unit.test.ts +++ b/packages/stack/src/versions.unit.test.ts @@ -1,4 +1,9 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { + readVersionManifestFromDockerfile, + syncDefaultVersionsSource, +} from "../scripts/sync-versions-from-dockerfile.ts"; import { DEFAULT_VERSIONS, diffPinnedAndAvailableVersions, @@ -9,6 +14,29 @@ import { type VersionManifest, } from "./versions.ts"; +const dockerfile = readFileSync( + new URL("../../../apps/cli-go/pkg/config/templates/Dockerfile", import.meta.url), + "utf8", +); + +const sampleDockerfile = ` +FROM supabase/postgres:17.0.0.1 AS pg +FROM library/kong:2.8.1 AS kong +FROM axllent/mailpit:v1.2.3 AS mailpit +FROM postgrest/postgrest:v14.0 AS postgrest +FROM supabase/postgres-meta:v0.90.0 AS pgmeta +FROM supabase/studio:2026.01.01-sha-abcdef0 AS studio +FROM darthsim/imgproxy:v3.8.0 AS imgproxy +FROM supabase/edge-runtime:v1.70.0 AS edgeruntime +FROM timberio/vector:0.50.0-alpine AS vector +FROM supabase/supavisor:2.1.0 AS supavisor +FROM supabase/gotrue:v2.100.0 AS gotrue +FROM supabase/realtime:v2.100.0 AS realtime +FROM supabase/storage-api:v1.50.0 AS storage +FROM supabase/logflare:1.40.0 AS logflare +FROM supabase/migra:3.0.1663481299 AS migra +`; + describe("DEFAULT_VERSIONS", () => { it("has all required services", () => { expect(DEFAULT_VERSIONS).toHaveProperty("postgres"); @@ -27,6 +55,65 @@ describe("DEFAULT_VERSIONS", () => { expect(typeof DEFAULT_VERSIONS["edge-runtime"]).toBe("string"); expect(DEFAULT_VERSIONS["edge-runtime"].length).toBeGreaterThan(0); }); + + it("matches the Dockerfile manifest exposed to Dependabot", () => { + expect(readVersionManifestFromDockerfile(dockerfile)).toEqual(DEFAULT_VERSIONS); + }); +}); + +describe("syncDefaultVersionsSource", () => { + it("rewrites the DEFAULT_VERSIONS block from Dockerfile versions", () => { + const source = `before +export const DEFAULT_VERSIONS: VersionManifest = { + postgres: "old", + postgrest: "old", + auth: "old", + "edge-runtime": "old", + realtime: "old", + storage: "old", + imgproxy: "old", + mailpit: "old", + pgmeta: "old", + studio: "old", + analytics: "old", + vector: "old", + pooler: "old", +} as const; +after`; + + expect(syncDefaultVersionsSource(source, readVersionManifestFromDockerfile(sampleDockerfile))) + .toMatchInlineSnapshot(` + "before + export const DEFAULT_VERSIONS: VersionManifest = { + postgres: "17.0.0.1", + postgrest: "14.0", + auth: "2.100.0", + "edge-runtime": "1.70.0", + realtime: "2.100.0", + storage: "1.50.0", + imgproxy: "v3.8.0", + mailpit: "v1.2.3", + pgmeta: "0.90.0", + studio: "2026.01.01-sha-abcdef0", + analytics: "1.40.0", + vector: "0.50.0-alpine", + pooler: "2.1.0", + } as const; + after" + `); + }); + + it("fails when a required Dockerfile image alias is missing", () => { + expect(() => + readVersionManifestFromDockerfile("FROM supabase/postgres:17.6.1.139 AS pg\n"), + ).toThrow("Missing Dockerfile versions for:"); + }); + + it("fails when the Dockerfile contains an unexpected image alias", () => { + expect(() => + readVersionManifestFromDockerfile(`${dockerfile}\nFROM supabase/example:1.0.0 AS example\n`), + ).toThrow("Unknown Dockerfile image alias 'example'."); + }); }); describe("dockerImageForService", () => { From 44b17996dacb2a9b740002df3232a96803052bcb Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Mon, 29 Jun 2026 20:57:56 +0200 Subject: [PATCH 02/12] chore(stack): sync service version defaults --- packages/stack/src/versions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index 6f784e874c..e837d7a4c9 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -50,8 +50,8 @@ export const DEFAULT_VERSIONS: VersionManifest = { postgrest: "14.13", auth: "2.191.0", "edge-runtime": "1.74.2", - realtime: "2.111.10", - storage: "1.61.4", + realtime: "2.112.0", + storage: "1.61.5", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", From b9c2765c967d967e6e23fc4d3f03f9f446c5e645 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 08:54:35 +0200 Subject: [PATCH 03/12] chore(stack): address service sync review --- .../workflows/sync-stack-service-versions.yml | 22 ++++++++------- tools/nx-plugins/src/test.plugin.ts | 27 +++++++++++++------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/sync-stack-service-versions.yml b/.github/workflows/sync-stack-service-versions.yml index 95f15ef4c5..0650e3e233 100644 --- a/.github/workflows/sync-stack-service-versions.yml +++ b/.github/workflows/sync-stack-service-versions.yml @@ -22,19 +22,11 @@ jobs: runs-on: blacksmith-2vcpu-ubuntu-2404 if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name steps: - - name: Generate token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.GH_APP_CLIENT_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - permission-contents: write - - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: Setup uses: ./.github/actions/setup @@ -45,7 +37,17 @@ jobs: run: pnpm sync:versions working-directory: packages/stack + - name: Generate token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.GH_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + permission-contents: write + - name: Commit synced stack service versions + env: + GH_APP_TOKEN: ${{ steps.app-token.outputs.token }} run: | if git diff --quiet -- packages/stack/src/versions.ts; then echo "Stack service versions are already synced." @@ -56,4 +58,4 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add packages/stack/src/versions.ts git commit -m "chore(stack): sync service version manifest" - git push + git push "https://x-access-token:${GH_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_HEAD_REF}" diff --git a/tools/nx-plugins/src/test.plugin.ts b/tools/nx-plugins/src/test.plugin.ts index 8d3f46e370..27eec91fa3 100644 --- a/tools/nx-plugins/src/test.plugin.ts +++ b/tools/nx-plugins/src/test.plugin.ts @@ -35,14 +35,24 @@ export const createNodesV2: CreateNodesV2 = [ const vitestProjects = vitestConfig.vitestConfig?.projects ?? []; if (vitestProjects.length > 0) { for (const vitestProject of vitestProjects) { - if (vitestProject.test) { + if (typeof vitestProject !== "string" && vitestProject.test) { + const testProject = vitestProject.test; + const targetName = testProject.name; + const extraInputs = + projectRoot === "packages/stack" && targetName === "unit" + ? ["{workspaceRoot}/apps/cli-go/pkg/config/templates/Dockerfile"] + : []; project.targets = { ...project.targets, - ...createTestTarget(vitestProject.test?.name, [ - ...(vitestProject?.test?.include ?? []), - ...(vitestProject?.test?.globalSetup ?? []), - ...(vitestProject?.test?.setupFiles ?? []), - ]), + ...createTestTarget( + targetName, + [ + ...(testProject.include ?? []), + ...(testProject.globalSetup ?? []), + ...(testProject.setupFiles ?? []), + ], + extraInputs, + ), }; } } @@ -62,7 +72,7 @@ export const createNodesV2: CreateNodesV2 = [ }, ]; -function createTestTarget(name: string = "", inputs: string[] = []) { +function createTestTarget(name: string = "", inputs: string[] = [], extraInputs: string[] = []) { return { [name !== "" ? `test:${name}` : "test"]: { command: `bun --bun vitest run${name !== "" ? ` --project ${name} --coverage.reportsDirectory=coverage/${name}` : ``}`, @@ -72,6 +82,7 @@ function createTestTarget(name: string = "", inputs: string[] = []) { "default", "sharedGlobals", ...inputs.map((input) => join(`{projectRoot}`, input)), + ...extraInputs, { externalDependencies: ["vitest"] }, ], }, @@ -79,5 +90,5 @@ function createTestTarget(name: string = "", inputs: string[] = []) { } function loadVitestDynamicImport() { - return Function('return import("vitest/node")')() as Promise; + return Function('return import("vitest/node")')(); } From eedfda9451ecf9a776ca86bd01a0e1a8251e1eb5 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 09:02:46 +0200 Subject: [PATCH 04/12] chore(stack): refresh service version defaults --- packages/stack/src/versions.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index e837d7a4c9..b560ca9534 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -46,17 +46,17 @@ export interface VersionManifest { } export const DEFAULT_VERSIONS: VersionManifest = { - postgres: "17.6.1.139", - postgrest: "14.13", - auth: "2.191.0", + postgres: "17.6.1.140", + postgrest: "14.14", + auth: "2.192.0", "edge-runtime": "1.74.2", - realtime: "2.112.0", - storage: "1.61.5", + realtime: "2.112.1", + storage: "1.61.7", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", - studio: "2026.06.22-sha-2207d7f", - analytics: "1.45.4", + studio: "2026.06.29-sha-20290c7", + analytics: "1.45.6", vector: "0.53.0-alpine", pooler: "2.9.7", } as const; From 900b6ee162d1032c76920e2ff5e41e4f4ffe3633 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 09:16:04 +0200 Subject: [PATCH 05/12] chore(stack): pin postgrest to available image --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- packages/stack/src/versions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 97dfc0dbea..7d5bcb1c34 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -3,7 +3,7 @@ FROM supabase/postgres:17.6.1.140 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.30.2 AS mailpit -FROM postgrest/postgrest:v14.14 AS postgrest +FROM postgrest/postgrest:v14.13 AS postgrest FROM supabase/postgres-meta:v0.96.6 AS pgmeta FROM supabase/studio:2026.06.29-sha-20290c7 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index b560ca9534..ce852476e2 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -47,7 +47,7 @@ export interface VersionManifest { export const DEFAULT_VERSIONS: VersionManifest = { postgres: "17.6.1.140", - postgrest: "14.14", + postgrest: "14.13", auth: "2.192.0", "edge-runtime": "1.74.2", realtime: "2.112.1", From 0ac93ddc48496174f4fbd412c395296890d3c635 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 09:19:21 +0200 Subject: [PATCH 06/12] chore(cli): refresh generated api types --- apps/cli-go/pkg/api/types.gen.go | 37 ++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go index 16267c8dc4..838e1e776a 100644 --- a/apps/cli-go/pkg/api/types.gen.go +++ b/apps/cli-go/pkg/api/types.gen.go @@ -1427,6 +1427,7 @@ const ( // Defines values for V1ListEntitlementsResponseEntitlementsFeatureKey. const ( + V1ListEntitlementsResponseEntitlementsFeatureKeyApiMembersRoles V1ListEntitlementsResponseEntitlementsFeatureKey = "api.members.roles" V1ListEntitlementsResponseEntitlementsFeatureKeyAssistantAdvanceModel V1ListEntitlementsResponseEntitlementsFeatureKey = "assistant.advance_model" V1ListEntitlementsResponseEntitlementsFeatureKeyAuditLogDrains V1ListEntitlementsResponseEntitlementsFeatureKey = "audit_log_drains" V1ListEntitlementsResponseEntitlementsFeatureKeyAuthAdvancedAuthSettings V1ListEntitlementsResponseEntitlementsFeatureKey = "auth.advanced_auth_settings" @@ -1485,6 +1486,7 @@ const ( V1ListEntitlementsResponseEntitlementsFeatureKeyStorageImageTransformations V1ListEntitlementsResponseEntitlementsFeatureKey = "storage.image_transformations" V1ListEntitlementsResponseEntitlementsFeatureKeyStorageMaxFileSize V1ListEntitlementsResponseEntitlementsFeatureKey = "storage.max_file_size" V1ListEntitlementsResponseEntitlementsFeatureKeyStorageMaxFileSizeConfigurable V1ListEntitlementsResponseEntitlementsFeatureKey = "storage.max_file_size.configurable" + V1ListEntitlementsResponseEntitlementsFeatureKeyStoragePurgeCache V1ListEntitlementsResponseEntitlementsFeatureKey = "storage.purge_cache" V1ListEntitlementsResponseEntitlementsFeatureKeyStorageVectorBuckets V1ListEntitlementsResponseEntitlementsFeatureKey = "storage.vector_buckets" V1ListEntitlementsResponseEntitlementsFeatureKeyVanitySubdomain V1ListEntitlementsResponseEntitlementsFeatureKey = "vanity_subdomain" ) @@ -3488,10 +3490,13 @@ type PostgrestConfigWithJWTSecretResponse struct { DbExtraSearchPath string `json:"db_extra_search_path"` // DbPool If `null`, the value is automatically configured based on compute size. - DbPool nullable.Nullable[int] `json:"db_pool"` - DbSchema string `json:"db_schema"` - JwtSecret *string `json:"jwt_secret,omitempty"` - MaxRows int `json:"max_rows"` + DbPool nullable.Nullable[int] `json:"db_pool"` + + // DbPoolAcquisitionTimeout If `null`, the value is automatically configured to 10. + DbPoolAcquisitionTimeout nullable.Nullable[int] `json:"db_pool_acquisition_timeout"` + DbSchema string `json:"db_schema"` + JwtSecret *string `json:"jwt_secret,omitempty"` + MaxRows int `json:"max_rows"` } // ProjectClaimTokenResponse defines model for ProjectClaimTokenResponse. @@ -3961,6 +3966,9 @@ type StorageConfigResponse struct { ImageTransformation struct { Enabled bool `json:"enabled"` } `json:"imageTransformation"` + PurgeCache struct { + Enabled bool `json:"enabled"` + } `json:"purgeCache"` S3Protocol struct { Enabled bool `json:"enabled"` } `json:"s3Protocol"` @@ -4563,6 +4571,9 @@ type UpdateStorageConfigBody struct { ImageTransformation *struct { Enabled bool `json:"enabled"` } `json:"imageTransformation,omitempty"` + PurgeCache *struct { + Enabled bool `json:"enabled"` + } `json:"purgeCache,omitempty"` S3Protocol *struct { Enabled bool `json:"enabled"` } `json:"s3Protocol,omitempty"` @@ -4909,9 +4920,12 @@ type V1PostgrestConfigResponse struct { DbExtraSearchPath string `json:"db_extra_search_path"` // DbPool If `null`, the value is automatically configured based on compute size. - DbPool nullable.Nullable[int] `json:"db_pool"` - DbSchema string `json:"db_schema"` - MaxRows int `json:"max_rows"` + DbPool nullable.Nullable[int] `json:"db_pool"` + + // DbPoolAcquisitionTimeout If `null`, the value is automatically configured to 10. + DbPoolAcquisitionTimeout nullable.Nullable[int] `json:"db_pool_acquisition_timeout"` + DbSchema string `json:"db_schema"` + MaxRows int `json:"max_rows"` } // V1ProfileResponse defines model for V1ProfileResponse. @@ -5166,10 +5180,11 @@ type V1UpdatePasswordResponse struct { // V1UpdatePostgrestConfigBody defines model for V1UpdatePostgrestConfigBody. type V1UpdatePostgrestConfigBody struct { - DbExtraSearchPath *string `json:"db_extra_search_path,omitempty"` - DbPool *int `json:"db_pool,omitempty"` - DbSchema *string `json:"db_schema,omitempty"` - MaxRows *int `json:"max_rows,omitempty"` + DbExtraSearchPath *string `json:"db_extra_search_path,omitempty"` + DbPool *int `json:"db_pool,omitempty"` + DbPoolAcquisitionTimeout *int `json:"db_pool_acquisition_timeout,omitempty"` + DbSchema *string `json:"db_schema,omitempty"` + MaxRows *int `json:"max_rows,omitempty"` } // V1UpdateProjectBody defines model for V1UpdateProjectBody. From 8b6974e482ba25fae66e66584dc2c6a04875cfc3 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 09:26:08 +0200 Subject: [PATCH 07/12] chore(cli): handle generated storage purge cache field --- apps/cli-go/pkg/config/storage.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/cli-go/pkg/config/storage.go b/apps/cli-go/pkg/config/storage.go index 740c525f43..14bf5ff082 100644 --- a/apps/cli-go/pkg/config/storage.go +++ b/apps/cli-go/pkg/config/storage.go @@ -73,6 +73,9 @@ func (s *storage) ToUpdateStorageConfigBody() v1API.UpdateStorageConfigBody { ImageTransformation *struct { Enabled bool `json:"enabled"` } `json:"imageTransformation,omitempty"` + PurgeCache *struct { + Enabled bool `json:"enabled"` + } `json:"purgeCache,omitempty"` S3Protocol *struct { Enabled bool `json:"enabled"` } `json:"s3Protocol,omitempty"` From 87082820db7a0cdefd40850c21bee70fefc0af23 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 09:37:55 +0200 Subject: [PATCH 08/12] chore(stack): pin logflare to available image --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- packages/stack/src/versions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 7d5bcb1c34..2ef33edc70 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -13,7 +13,7 @@ FROM supabase/supavisor:2.9.7 AS supavisor FROM supabase/gotrue:v2.192.0 AS gotrue FROM supabase/realtime:v2.112.1 AS realtime FROM supabase/storage-api:v1.61.7 AS storage -FROM supabase/logflare:1.45.6 AS logflare +FROM supabase/logflare:1.45.4 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ FROM supabase/migra:3.0.1663481299 AS migra diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index ce852476e2..5a3203f23b 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -56,7 +56,7 @@ export const DEFAULT_VERSIONS: VersionManifest = { mailpit: "v1.30.2", pgmeta: "0.96.6", studio: "2026.06.29-sha-20290c7", - analytics: "1.45.6", + analytics: "1.45.4", vector: "0.53.0-alpine", pooler: "2.9.7", } as const; From 2e8a2788deb44ca2e0b9e6d0d07abc3ec81011cf Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 10:01:08 +0200 Subject: [PATCH 09/12] chore(cli): make stop cleanup idempotent --- .../src/next/commands/stop/stop.handler.ts | 15 +++++++++-- .../commands/stop/stop.integration.test.ts | 27 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/next/commands/stop/stop.handler.ts b/apps/cli/src/next/commands/stop/stop.handler.ts index b7e9dd033a..84d8801f59 100644 --- a/apps/cli/src/next/commands/stop/stop.handler.ts +++ b/apps/cli/src/next/commands/stop/stop.handler.ts @@ -16,20 +16,31 @@ export const stop = Effect.fnUntraced(function* (flags: StopFlags) { yield* output.intro("Stop local Supabase stack"); if (flags.noBackup) { + let stoppedRunningStack = true; yield* stopDaemon({ cwd, cacheRoot: cliConfig.supabaseHome, projectDir: projectHome.projectRoot, projectStateRoot: projectHome.projectHomeDir, name: flags.stack, - }).pipe(Effect.catchTag("NoRunningStackError", () => Effect.void)); + }).pipe( + Effect.catchTag("NoRunningStackError", () => + Effect.sync(() => { + stoppedRunningStack = false; + }), + ), + ); yield* deleteManagedStackPersistence({ cwd, cacheRoot: cliConfig.supabaseHome, projectDir: projectHome.projectRoot, projectStateRoot: projectHome.projectHomeDir, name: flags.stack, - }); + }).pipe( + Effect.catchTag("NoRunningStackError", (error) => + stoppedRunningStack ? Effect.void : Effect.fail(error), + ), + ); yield* output.success("Local Supabase stopped and persisted data deleted"); yield* output.outro("Local Supabase stack stopped and local data deleted."); diff --git a/apps/cli/src/next/commands/stop/stop.integration.test.ts b/apps/cli/src/next/commands/stop/stop.integration.test.ts index 77173df170..bcf7a8d46f 100644 --- a/apps/cli/src/next/commands/stop/stop.integration.test.ts +++ b/apps/cli/src/next/commands/stop/stop.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { existsSync, mkdtempSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Effect, Exit, Layer } from "effect"; @@ -80,6 +80,31 @@ describe("stop handler", () => { }), ); + it.live("treats already-removed persistence as success after stopping with --no-backup", () => + Effect.gen(function* () { + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => makeRunningStackFixture()), + (resource) => Effect.promise(() => resource.dispose()), + ); + const out = mockOutput(); + const layer = Layer.mergeAll(fixture.baseLayer, out.layer); + + rmSync(fixture.stackMetadataPath, { force: true }); + + yield* stop({ stack: fixture.stackName, noBackup: true }).pipe(Effect.provide(layer)); + + expect(fixture.stopped).toBe(true); + expect(existsSync(fixture.stackStatePath)).toBe(false); + expect(existsSync(fixture.stackMetadataPath)).toBe(false); + expect(out.messages).toContainEqual( + expect.objectContaining({ + type: "success", + message: "Local Supabase stopped and persisted data deleted", + }), + ); + }), + ); + it.live("deletes the requested stopped named stack with --no-backup", () => Effect.gen(function* () { const fixture = yield* Effect.acquireRelease( From 74f6cf9e5bee0ff29e202db4e83be5603152319c Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 10:08:23 +0200 Subject: [PATCH 10/12] test(cli): show stop e2e output on failure --- apps/cli/src/next/commands/stop/stop.e2e.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/next/commands/stop/stop.e2e.test.ts b/apps/cli/src/next/commands/stop/stop.e2e.test.ts index a63661a6e3..49881e8ed8 100644 --- a/apps/cli/src/next/commands/stop/stop.e2e.test.ts +++ b/apps/cli/src/next/commands/stop/stop.e2e.test.ts @@ -68,7 +68,10 @@ describe("supabase stop", () => { cwd: project.dir, home: home.dir, }); - expect(stopResult.exitCode).toBe(0); + expect( + stopResult.exitCode, + `stdout:\n${stopResult.stdout}\n\nstderr:\n${stopResult.stderr}`, + ).toBe(0); expect(existsSync(stackDir)).toBe(false); }, ); From 18560370ba0112b4e4ae414828d7f02d5ad50b13 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 10:22:23 +0200 Subject: [PATCH 11/12] chore(stack): harden no-backup cleanup --- packages/stack/src/StateManager.ts | 80 ++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/stack/src/StateManager.ts b/packages/stack/src/StateManager.ts index 27303a9214..5b2b7e6c40 100644 --- a/packages/stack/src/StateManager.ts +++ b/packages/stack/src/StateManager.ts @@ -1,5 +1,7 @@ import { Data, Effect, Layer, Schema, Context } from "effect"; import { FileSystem, Path } from "effect"; +import { execFileSync } from "node:child_process"; +import { existsSync, rmSync } from "node:fs"; import { AllocatedPortsSchema, type AllocatedPorts } from "./PortAllocator.ts"; import { PartialVersionManifestSchema, @@ -14,7 +16,7 @@ import { defaultManagedRuntimeRoot, socketPathForRuntimeRoot, } from "./paths.ts"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; // --------------------------------------------------------------------------- // Types @@ -460,9 +462,81 @@ function makeRemove(deps: StateManagerDeps) { function makeDeleteStack(deps: StateManagerDeps) { return (name: string): Effect.Effect => Effect.gen(function* () { - yield* deps.fs.remove(deps.stackDir(name), { recursive: true }); + const stackDir = deps.stackDir(name); + yield* deps.fs + .remove(stackDir, { recursive: true }) + .pipe(Effect.catch((error) => removeStackDirWithDocker(stackDir, error))); yield* deps.fs.remove(deps.runtimeDir(name), { recursive: true }).pipe(Effect.ignore); - }).pipe(Effect.catchTag("PlatformError", (e) => Effect.die(e))); + }); +} + +function localDockerImages(): ReadonlyArray { + try { + return execFileSync("docker", ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5_000, + }) + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.endsWith(":")); + } catch { + return []; + } +} + +function postgresCleanupImages(): ReadonlyArray { + return localDockerImages().filter( + (image) => + image.startsWith("public.ecr.aws/supabase/postgres:") || + image.startsWith("supabase/postgres:") || + image.startsWith("ghcr.io/supabase/postgres:"), + ); +} + +function dockerRemovePath(targetPath: string, image: string): void { + execFileSync( + "docker", + [ + "run", + "--rm", + "--user", + "0:0", + "-v", + `${dirname(targetPath)}:/parent`, + "-e", + `TARGET_NAME=${basename(targetPath)}`, + "--entrypoint", + "sh", + image, + "-c", + 'cd /parent && rm -rf -- "$TARGET_NAME"', + ], + { stdio: "ignore", timeout: 30_000 }, + ); +} + +function removeStackDirWithDocker(targetPath: string, cause: unknown): Effect.Effect { + return Effect.sync(() => { + try { + rmSync(targetPath, { recursive: true, force: true }); + } catch {} + + if (!existsSync(targetPath)) { + return; + } + + for (const image of postgresCleanupImages()) { + try { + dockerRemovePath(targetPath, image); + } catch {} + if (!existsSync(targetPath)) { + return; + } + } + + throw cause; + }); } function makeResolve( From c4ed1b97c4f411514380e6b275d5a729cf971b55 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 30 Jun 2026 10:31:49 +0200 Subject: [PATCH 12/12] chore(stack): align postgres native version --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- packages/stack/src/versions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 2ef33edc70..90227b830c 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.6.1.140 AS pg +FROM supabase/postgres:17.6.1.141 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.30.2 AS mailpit diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index 5a3203f23b..6a25874e72 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -46,7 +46,7 @@ export interface VersionManifest { } export const DEFAULT_VERSIONS: VersionManifest = { - postgres: "17.6.1.140", + postgres: "17.6.1.141", postgrest: "14.13", auth: "2.192.0", "edge-runtime": "1.74.2",