chore(stack): sync service versions from Dockerfile (#5716) · pascalarth-max/cli@03e0f9d · GitHub
Skip to content

Commit 03e0f9d

Browse files
authored
chore(stack): sync service versions from Dockerfile (supabase#5716)
Adds a stack-owned sync script that derives `DEFAULT_VERSIONS` from the Dockerfile manifest Dependabot already updates, and adds a Dependabot-only workflow that pushes the synced manifest back to Docker image update PRs with the existing GitHub App token pattern. Also syncs the current `DEFAULT_VERSIONS` values to the Dockerfile manifest and makes version-sensitive tests derive expectations from the manifest defaults where appropriate. Linear: CLI-1589
1 parent 11e59d2 commit 03e0f9d

13 files changed

Lines changed: 450 additions & 42 deletions
Lines changed: 61 additions & 0 deletions

‎apps/cli/src/next/commands/stop/stop.e2e.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ describe("supabase stop", () => {
6868
cwd: project.dir,
6969
home: home.dir,
7070
});
71-
expect(stopResult.exitCode).toBe(0);
71+
expect(
72+
stopResult.exitCode,
73+
`stdout:\n${stopResult.stdout}\n\nstderr:\n${stopResult.stderr}`,
74+
).toBe(0);
7275
expect(existsSync(stackDir)).toBe(false);
7376
},
7477
);

‎apps/cli/src/next/commands/stop/stop.handler.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,31 @@ export const stop = Effect.fnUntraced(function* (flags: StopFlags) {
1616
yield* output.intro("Stop local Supabase stack");
1717

1818
if (flags.noBackup) {
19+
let stoppedRunningStack = true;
1920
yield* stopDaemon({
2021
cwd,
2122
cacheRoot: cliConfig.supabaseHome,
2223
projectDir: projectHome.projectRoot,
2324
projectStateRoot: projectHome.projectHomeDir,
2425
name: flags.stack,
25-
}).pipe(Effect.catchTag("NoRunningStackError", () => Effect.void));
26+
}).pipe(
27+
Effect.catchTag("NoRunningStackError", () =>
28+
Effect.sync(() => {
29+
stoppedRunningStack = false;
30+
}),
31+
),
32+
);
2633
yield* deleteManagedStackPersistence({
2734
cwd,
2835
cacheRoot: cliConfig.supabaseHome,
2936
projectDir: projectHome.projectRoot,
3037
projectStateRoot: projectHome.projectHomeDir,
3138
name: flags.stack,
32-
});
39+
}).pipe(
40+
Effect.catchTag("NoRunningStackError", (error) =>
41+
stoppedRunningStack ? Effect.void : Effect.fail(error),
42+
),
43+
);
3344

3445
yield* output.success("Local Supabase stopped and persisted data deleted");
3546
yield* output.outro("Local Supabase stack stopped and local data deleted.");

‎apps/cli/src/next/commands/stop/stop.integration.test.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "@effect/vitest";
2-
import { existsSync, mkdtempSync } from "node:fs";
2+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import { Effect, Exit, Layer } from "effect";
@@ -80,6 +80,31 @@ describe("stop handler", () => {
8080
}),
8181
);
8282

83+
it.live("treats already-removed persistence as success after stopping with --no-backup", () =>
84+
Effect.gen(function* () {
85+
const fixture = yield* Effect.acquireRelease(
86+
Effect.promise(() => makeRunningStackFixture()),
87+
(resource) => Effect.promise(() => resource.dispose()),
88+
);
89+
const out = mockOutput();
90+
const layer = Layer.mergeAll(fixture.baseLayer, out.layer);
91+
92+
rmSync(fixture.stackMetadataPath, { force: true });
93+
94+
yield* stop({ stack: fixture.stackName, noBackup: true }).pipe(Effect.provide(layer));
95+
96+
expect(fixture.stopped).toBe(true);
97+
expect(existsSync(fixture.stackStatePath)).toBe(false);
98+
expect(existsSync(fixture.stackMetadataPath)).toBe(false);
99+
expect(out.messages).toContainEqual(
100+
expect.objectContaining({
101+
type: "success",
102+
message: "Local Supabase stopped and persisted data deleted",
103+
}),
104+
);
105+
}),
106+
);
107+
83108
it.live("deletes the requested stopped named stack with --no-backup", () =>
84109
Effect.gen(function* () {
85110
const fixture = yield* Effect.acquireRelease(

‎packages/stack/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test": "nx run-many -t test:core test:e2e --projects=$npm_package_name",
1616
"test:core": "nx run-many -t test:unit test:integration --projects=$npm_package_name",
1717
"test:e2e:warmup": "bun run tests/warmup-e2e.ts",
18+
"sync:versions": "bun run scripts/sync-versions-from-dockerfile.ts",
1819
"check:all": "nx run-many -t types:check lint:check fmt:check knip:check --projects=$npm_package_name",
1920
"fix:all": "nx run-many -t lint:fix fmt:fix knip:fix --projects=$npm_package_name"
2021
},
@@ -40,6 +41,7 @@
4041
},
4142
"knip": {
4243
"entry": [
44+
"scripts/**/*.ts",
4345
"src/**/*.test.ts",
4446
"src/daemon-node.ts",
4547
"tests/**/*.ts"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { readFile } from "node:fs/promises";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import {
5+
normalizeServiceVersion,
6+
SERVICE_NAMES,
7+
type ServiceName,
8+
type VersionManifest,
9+
} from "../src/versions.ts";
10+
11+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12+
const repoRoot = path.resolve(scriptDir, "../../..");
13+
const dockerfilePath = path.join(repoRoot, "apps/cli-go/pkg/config/templates/Dockerfile");
14+
const versionsPath = path.join(repoRoot, "packages/stack/src/versions.ts");
15+
16+
const fromLinePattern = /^FROM\s+(.+):([^:\s]+)\s+AS\s+([^\s#]+)/i;
17+
18+
const dockerfileAliases = new Map<string, ServiceName>([
19+
["pg", "postgres"],
20+
["postgrest", "postgrest"],
21+
["gotrue", "auth"],
22+
["edgeruntime", "edge-runtime"],
23+
["realtime", "realtime"],
24+
["storage", "storage"],
25+
["imgproxy", "imgproxy"],
26+
["mailpit", "mailpit"],
27+
["pgmeta", "pgmeta"],
28+
["studio", "studio"],
29+
["logflare", "analytics"],
30+
["vector", "vector"],
31+
["supavisor", "pooler"],
32+
]);
33+
34+
const ignoredAliases = new Set(["kong", "differ", "migra", "pgprove"]);
35+
36+
function assertFullManifest(
37+
versions: Partial<Record<ServiceName, string>>,
38+
): asserts versions is VersionManifest {
39+
const missing = SERVICE_NAMES.filter((service) => versions[service] === undefined);
40+
if (missing.length > 0) {
41+
throw new Error(`Missing Dockerfile versions for: ${missing.join(", ")}`);
42+
}
43+
}
44+
45+
export function readVersionManifestFromDockerfile(dockerfile: string): VersionManifest {
46+
const versions: Partial<Record<ServiceName, string>> = {};
47+
48+
for (const rawLine of dockerfile.split("\n")) {
49+
const line = rawLine.trim();
50+
const match = fromLinePattern.exec(line);
51+
if (match === null) {
52+
continue;
53+
}
54+
55+
const [, , tag, alias] = match;
56+
if (tag === undefined || alias === undefined) {
57+
continue;
58+
}
59+
60+
if (ignoredAliases.has(alias)) {
61+
continue;
62+
}
63+
64+
const service = dockerfileAliases.get(alias);
65+
if (service === undefined) {
66+
throw new Error(`Unknown Dockerfile image alias '${alias}'.`);
67+
}
68+
if (versions[service] !== undefined) {
69+
throw new Error(`Duplicate Dockerfile version for '${service}'.`);
70+
}
71+
72+
versions[service] = normalizeServiceVersion(service, tag);
73+
}
74+
75+
assertFullManifest(versions);
76+
return versions;
77+
}
78+
79+
function renderManifestKey(service: ServiceName): string {
80+
return /^[a-zA-Z_$][\w$]*$/.test(service) ? service : JSON.stringify(service);
81+
}
82+
83+
export function renderDefaultVersions(versions: VersionManifest): string {
84+
const lines = SERVICE_NAMES.map(
85+
(service) => ` ${renderManifestKey(service)}: ${JSON.stringify(versions[service])},`,
86+
);
87+
return ["export const DEFAULT_VERSIONS: VersionManifest = {", ...lines, "} as const;"].join("\n");
88+
}
89+
90+
export function syncDefaultVersionsSource(source: string, versions: VersionManifest): string {
91+
const startMarker = "export const DEFAULT_VERSIONS: VersionManifest = {";
92+
const endMarker = "\n} as const;";
93+
const start = source.indexOf(startMarker);
94+
if (start === -1) {
95+
throw new Error("Could not find DEFAULT_VERSIONS declaration.");
96+
}
97+
98+
const end = source.indexOf(endMarker, start);
99+
if (end === -1) {
100+
throw new Error("Could not find DEFAULT_VERSIONS declaration end.");
101+
}
102+
103+
return `${source.slice(0, start)}${renderDefaultVersions(versions)}${source.slice(
104+
end + endMarker.length,
105+
)}`;
106+
}
107+
108+
async function main() {
109+
const checkOnly = process.argv.includes("--check");
110+
const dockerfile = await readFile(dockerfilePath, "utf8");
111+
const versionsSource = await readFile(versionsPath, "utf8");
112+
const versions = readVersionManifestFromDockerfile(dockerfile);
113+
const syncedSource = syncDefaultVersionsSource(versionsSource, versions);
114+
115+
if (syncedSource === versionsSource) {
116+
console.log("DEFAULT_VERSIONS is already synced with the Dockerfile manifest.");
117+
return;
118+
}
119+
120+
if (checkOnly) {
121+
console.error("DEFAULT_VERSIONS is out of sync with the Dockerfile manifest.");
122+
process.exitCode = 1;
123+
return;
124+
}
125+
126+
await Bun.write(versionsPath, syncedSource);
127+
console.log("Synced DEFAULT_VERSIONS with the Dockerfile manifest.");
128+
}
129+
130+
if (import.meta.main) {
131+
await main();
132+
}

‎packages/stack/src/BinaryResolver.unit.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DEFAULT_VERSIONS } from "./versions.ts";
55
const postgresVersion = DEFAULT_VERSIONS.postgres;
66
const postgrestVersion = DEFAULT_VERSIONS.postgrest;
77
const authVersion = DEFAULT_VERSIONS.auth;
8+
const authRcVersion = "2.188.0-rc.15";
89
const edgeRuntimeVersion = DEFAULT_VERSIONS["edge-runtime"];
910

1011
describe("BinaryResolver.downloadUrl", () => {
@@ -44,11 +45,11 @@ describe("BinaryResolver.downloadUrl", () => {
4445
it("constructs auth URL for rc releases", () => {
4546
const url = BinaryResolver.downloadUrl({
4647
service: "auth",
47-
version: authVersion,
48+
version: authRcVersion,
4849
assetName: "arm64",
4950
});
5051
expect(url).toBe(
51-
`https://github.com/supabase/auth/releases/download/rc${authVersion}/auth-v${authVersion}-arm64.tar.gz`,
52+
`https://github.com/supabase/auth/releases/download/rc${authRcVersion}/auth-v${authRcVersion}-arm64.tar.gz`,
5253
);
5354
});
5455

‎packages/stack/src/StackBuilder.unit.test.ts‎

Lines changed: 4 additions & 2 deletions

0 commit comments

Comments
 (0)