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
61 changes: 61 additions & 0 deletions .github/workflows/sync-stack-service-versions.yml
Original file line number Diff line number Diff line change
@@ -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}"
5 changes: 4 additions & 1 deletion apps/cli/src/next/commands/stop/stop.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
);
Expand Down
15 changes: 13 additions & 2 deletions apps/cli/src/next/commands/stop/stop.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
27 changes: 26 additions & 1 deletion apps/cli/src/next/commands/stop/stop.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/stack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -40,6 +41,7 @@
},
"knip": {
"entry": [
"scripts/**/*.ts",
"src/**/*.test.ts",
"src/daemon-node.ts",
"tests/**/*.ts"
Expand Down
132 changes: 132 additions & 0 deletions packages/stack/scripts/sync-versions-from-dockerfile.ts
Original file line number Diff line number Diff line change
@@ -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<string, ServiceName>([
["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<Record<ServiceName, string>>,
): 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<Record<ServiceName, string>> = {};

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();
}
5 changes: 3 additions & 2 deletions packages/stack/src/BinaryResolver.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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`,
);
});

Expand Down
6 changes: 4 additions & 2 deletions packages/stack/src/StackBuilder.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
Loading
Loading