Skip to content
Draft
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
59 changes: 59 additions & 0 deletions .github/workflows/sync-stack-service-versions.yml
Original file line number Diff line number Diff line change
@@ -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
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));
});
});
23 changes: 11 additions & 12 deletions packages/stack/src/prefetch.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
]);
});

Expand Down Expand Up @@ -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([]);
});
Expand Down
22 changes: 11 additions & 11 deletions packages/stack/src/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`). */
Expand Down
Loading