diff --git a/.github/workflows/sync-stack-service-versions.yml b/.github/workflows/sync-stack-service-versions.yml new file mode 100644 index 0000000000..0650e3e233 --- /dev/null +++ b/.github/workflows/sync-stack-service-versions.yml @@ -0,0 +1,61 @@ +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: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.ref }} + persist-credentials: false + + - 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: 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." + 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 "https://x-access-token:${GH_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_HEAD_REF}" 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); }, ); 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( diff --git a/packages/stack/package.json b/packages/stack/package.json index 54aff19c81..284f92a896 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/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( 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..2141ee733a 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.141", + postgrest: "14.14", + auth: "2.192.0", + "edge-runtime": "1.74.2", + realtime: "2.112.2", + storage: "1.61.9", 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.29-sha-20290c7", + analytics: "1.45.6", + 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", () => { 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")')(); }