From 524cba46f6b6c015b999597ba10cb415b939793b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:07:47 +0000 Subject: [PATCH 1/3] ci: unbreak dependabot Dockerfile bumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dependabot's docker updates on apps/cli-go/pkg/config/templates/Dockerfile have been landing red. Two independent causes: 1. slim-images.unit.test.ts asserted the exact current pin for pg, supavisor, realtime, and storage while reading those pins from the live Dockerfile manifest. Every bump of one of those four images therefore failed the unit suite (#6502 on realtime, #6503 on postgres). The assertions that must track the manifest — which slim repository each alias maps to — already live in the it.each above and slice the tag off, so the version-bearing block only encoded the tag-prefix scheme. Replace it with fixed pins that cover the same scheme, including the uppercase-V normalization arm that had no coverage. 2. sync-stack-service-versions.yml ran `pnpm sync:versions` in packages/stack and committed packages/stack/src/ServiceCatalog.ts. The managed local stack runtime rewrite (#6440) deleted both the script and that file, so the workflow would now hard-fail on the next dependabot Dockerfile PR. Its replacement, WorkloadCatalog.ts, pins slim ghcr.io/supabase/cli images by digest and already runs ahead of the Dockerfile on its own release train, so a Dockerfile-driven sync is no longer the right shape for it. Drop the workflow rather than repoint it; nothing else referenced it. Verified by replaying the #6502 and #6503 bumps onto the Dockerfile locally and running the apps/cli unit suite. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PWXmTVTiQCwuyjZ8Mfw9NL --- .../workflows/sync-stack-service-versions.yml | 61 ------------------- .../shared/services/slim-images.unit.test.ts | 20 +++--- 2 files changed, 11 insertions(+), 70 deletions(-) delete mode 100644 .github/workflows/sync-stack-service-versions.yml diff --git a/.github/workflows/sync-stack-service-versions.yml b/.github/workflows/sync-stack-service-versions.yml deleted file mode 100644 index b8cdb52e6e..0000000000 --- a/.github/workflows/sync-stack-service-versions.yml +++ /dev/null @@ -1,61 +0,0 @@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - 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/ServiceCatalog.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/ServiceCatalog.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/shared/services/slim-images.unit.test.ts b/apps/cli/src/shared/services/slim-images.unit.test.ts index ad80353d25..54f8921b40 100644 --- a/apps/cli/src/shared/services/slim-images.unit.test.ts +++ b/apps/cli/src/shared/services/slim-images.unit.test.ts @@ -43,19 +43,21 @@ describe("toSlimImage", () => { ); }); - it("maps current docker.io pins onto the published slim tags", () => { - expect(toSlimImage("pg", dockerfileServiceImageRaw("pg"))).toBe( - "ghcr.io/supabase/cli/postgres:17.6.1.167", - ); - expect(toSlimImage("supavisor", dockerfileServiceImageRaw("supavisor"))).toBe( - "ghcr.io/supabase/cli/pooler:v2.9.12", - ); - expect(toSlimImage("realtime", dockerfileServiceImageRaw("realtime"))).toBe( + // Tag-scheme assertions use fixed pins rather than the Dockerfile manifest: + // dependabot bumps that manifest, and an expectation spelling out the current + // pin would turn every bump into a failing test. The `it.each` above keeps the + // live manifest covered for the part that must track it — the repository each + // alias maps to. + it("keeps a single v on pins already prefixed on docker.io", () => { + expect(toSlimImage("realtime", "supabase/realtime:v2.130.0")).toBe( "ghcr.io/supabase/cli/realtime:v2.130.0", ); - expect(toSlimImage("storage", dockerfileServiceImageRaw("storage"))).toBe( + expect(toSlimImage("storage", "supabase/storage-api:v1.72.1")).toBe( "ghcr.io/supabase/cli/storage:v1.72.1", ); + expect(toSlimImage("gotrue", "supabase/gotrue:V2.196.0")).toBe( + "ghcr.io/supabase/cli/auth:v2.196.0", + ); }); it("v-prefixes pins whose slim tag scheme differs from docker.io's", () => { From b1708051e2e87173731cfbf58a6531b56726f810 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:33:59 +0000 Subject: [PATCH 2/3] ci(stack): sync workload catalog from the slim-services release feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the automation gap left by removing sync-stack-service-versions.yml in the previous commit. That workflow synced packages/stack/src/ServiceCatalog.ts from the Dockerfile. The managed stack runtime rewrite (#6440) deleted that file; its replacement, WorkloadCatalog.ts, pins each workload to an exact slim-services artifact release — a version plus its ghcr.io/supabase/cli image digest — per ADR 0017, which makes the artifact release the boundary for service startup defaults. Dependabot owns the Dockerfile and structurally cannot own this table: it resolves registry tags and never produces a sha256 digest. So the catalog now rides the feed that does carry digests. slim-services already sends this repo a `mirror-slim-image` repository_dispatch per release, carrying service/version/digest, to drive the ECR mirror. The new workflow subscribes to that same dispatch and opens a PR pinning the release. It is deliberately separate from mirror-slim-image.yml: that mirror runs against the sender's 15-minute verification poll, and a catalog PR must never delay it or turn its run red. Only two source values change per release — artifactFor derives the native release tag, asset names, and every download URL from service + version, and `releases` is derived from defaultVersion plus the container image. The payload arrives with whatever authority holds the dispatch token, so sync-workload-catalog.ts revalidates service/version/digest against the same patterns mirror-slim-image.yml uses; those patterns are what stop a version or digest breaking out of the TypeScript string literals it writes into. postgres is the one service carrying two supported release lines (17.x and 15.x), so the plan picks its target by release line: a 15.x release moves the additionalReleases entry and can never overwrite the 17.x default. A release on a line the catalog does not carry, and a service it does not model, are both successful no-ops. Re-dispatches are no-ops too, so the sender's retry path does not open duplicate PRs. planCatalogUpdate is pure and covered by 21 bun:test cases, including the postgres line-targeting guard and a sweep that drives every service the real catalog models. Verified end-to-end by running the script against the real catalog for a single-line bump, a re-dispatch, a 15.x postgres release, an unmodelled service, and an invalid digest, then type-checking the rewritten catalog and running its 61 tests. Also records the pins' provenance in WorkloadCatalog.ts, since it and the Dockerfile list overlapping service versions and are meant to diverge. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PWXmTVTiQCwuyjZ8Mfw9NL --- .github/scripts/sync-workload-catalog.test.ts | 301 ++++++++++++++++++ .github/scripts/sync-workload-catalog.ts | 248 +++++++++++++++ .../workflows/sync-stack-workload-catalog.yml | 138 ++++++++ packages/stack/src/model/WorkloadCatalog.ts | 16 +- 4 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/sync-workload-catalog.test.ts create mode 100644 .github/scripts/sync-workload-catalog.ts create mode 100644 .github/workflows/sync-stack-workload-catalog.yml diff --git a/.github/scripts/sync-workload-catalog.test.ts b/.github/scripts/sync-workload-catalog.test.ts new file mode 100644 index 0000000000..74d88cbc0c --- /dev/null +++ b/.github/scripts/sync-workload-catalog.test.ts @@ -0,0 +1,301 @@ +import { describe, expect, test } from "bun:test"; + +import { + CATALOG_PATH, + InvalidPayloadError, + planCatalogUpdate, + releaseLine, + validatePayload, +} from "./sync-workload-catalog.ts"; + +const DIGEST_A = `sha256:${"a".repeat(64)}`; +const DIGEST_B = `sha256:${"b".repeat(64)}`; + +/** + * The real catalog, so these tests fail if its shape drifts away from what the + * patterns expect rather than passing against a stale hand-written fixture. + */ +const realCatalog = await Bun.file(CATALOG_PATH).text(); + +/** A trimmed catalog carrying both entry shapes the script has to rewrite. */ +const fixture = `const workloadCatalog = { + "database:database": native( + "postgres", + "17.6.1.168", + "ghcr.io/supabase/cli/postgres:17.6.1.168@sha256:${"9".repeat(64)}", + "bin/supabase-postgres-start", + ["bin/supabase-postgres-start"], + { + additionalReleases: { + "15.14.1.168": + "ghcr.io/supabase/cli/postgres:15.14.1.168@sha256:${"f".repeat(64)}", + }, + containerAlias: "supabase-database", + }, + ), + "rest:rest": native( + "postgrest", + "v16.2", + "ghcr.io/supabase/cli/postgrest:v16.2", + "bin/postgrest", + ["bin/postgrest"], + { containerAlias: "supabase-rest" }, + ), + "auth:auth": native("auth", "v2.196.0", "ghcr.io/supabase/cli/auth:v2.196.0", "bin/auth", [ + "bin/auth", + ]), + "studio:studio": native( + "studio", + "2026.09.04-sha-5a67366", + "ghcr.io/supabase/cli/studio:2026.09.04-sha-5a67366@sha256:${"c".repeat(64)}", + "bin/studio", + ["bin/studio"], + ), +} satisfies Readonly>; +`; + +describe("releaseLine", () => { + test.each([ + ["17.6.1.168", "17"], + ["15.14.1.168", "15"], + ["v2.196.0", "2"], + ["v16.2", "16"], + ["2026.09.04-sha-5a67366", "2026"], + ["0.53.0", "0"], + ])("reads %s as line %s", (version, expected) => { + expect(releaseLine(version)).toBe(expected); + }); +}); + +describe("validatePayload", () => { + test("rejects a version that would escape the string literal", () => { + expect(() => + validatePayload({ service: "auth", version: 'v1",\n "pwned', digest: DIGEST_A }), + ).toThrow(InvalidPayloadError); + }); + + test("rejects a non-sha256 digest", () => { + expect(() => validatePayload({ service: "auth", version: "v1.0.0", digest: "latest" })).toThrow( + InvalidPayloadError, + ); + }); + + test("rejects an uppercase or path-traversing service name", () => { + expect(() => + validatePayload({ service: "../../etc", version: "v1.0.0", digest: DIGEST_A }), + ).toThrow(InvalidPayloadError); + expect(() => validatePayload({ service: "Auth", version: "v1.0.0", digest: DIGEST_A })).toThrow( + InvalidPayloadError, + ); + }); +}); + +describe("planCatalogUpdate", () => { + test("bumps a single-line service and pins the digest", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "auth", + version: "v2.197.0", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.previousVersion).toBe("v2.196.0"); + expect(plan.target).toBe("default"); + expect(plan.source).toContain( + `native("auth", "v2.197.0", "ghcr.io/supabase/cli/auth:v2.197.0@${DIGEST_A}", "bin/auth"`, + ); + expect(plan.source).not.toContain("v2.196.0"); + }); + + test("adds a digest to a previously tag-only pin", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "postgrest", + version: "v16.2", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.source).toContain(`"ghcr.io/supabase/cli/postgrest:v16.2@${DIGEST_A}"`); + }); + + test("bumps a date-versioned service across a year boundary", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "studio", + version: "2027.01.02-sha-abc1234", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.source).toContain( + `"ghcr.io/supabase/cli/studio:2027.01.02-sha-abc1234@${DIGEST_A}"`, + ); + }); + + test("a 17.x postgres release moves the default and leaves the 15.x line alone", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "postgres", + version: "17.6.1.169", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.previousVersion).toBe("17.6.1.168"); + expect(plan.target).toBe("default"); + expect(plan.source).toContain(`"postgres",\n "17.6.1.169",`); + expect(plan.source).toContain(`"ghcr.io/supabase/cli/postgres:17.6.1.169@${DIGEST_A}"`); + // The additional line must survive untouched. + expect(plan.source).toContain(`"15.14.1.168":`); + expect(plan.source).toContain(`sha256:${"f".repeat(64)}`); + }); + + test("a 15.x postgres release moves the additional line, never the 17.x default", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "postgres", + version: "15.14.1.169", + digest: DIGEST_B, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.previousVersion).toBe("15.14.1.168"); + expect(plan.target).toBe("additional"); + expect(plan.source).toContain(`"15.14.1.169":`); + expect(plan.source).toContain(`"ghcr.io/supabase/cli/postgres:15.14.1.169@${DIGEST_B}"`); + // The 17.x default is the regression this guards: it must not move. + expect(plan.source).toContain(`"postgres",\n "17.6.1.168",`); + expect(plan.source).toContain(`sha256:${"9".repeat(64)}`); + }); + + test("skips a postgres release line the catalog does not carry", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "postgres", + version: "16.4.1.001", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("unmodelled-release-line"); + if (plan.kind !== "unmodelled-release-line") return; + expect(plan.known).toEqual(["17.6.1.168", "15.14.1.168"]); + }); + + test("re-dispatching the same release is a no-op", () => { + const first = planCatalogUpdate({ + source: fixture, + service: "auth", + version: "v2.197.0", + digest: DIGEST_A, + }); + expect(first.kind).toBe("updated"); + if (first.kind !== "updated") return; + + expect( + planCatalogUpdate({ + source: first.source, + service: "auth", + version: "v2.197.0", + digest: DIGEST_A, + }).kind, + ).toBe("unchanged"); + }); + + test("a digest change on the same version still syncs", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "auth", + version: "v2.196.0", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.source).toContain(`auth:v2.196.0@${DIGEST_A}`); + }); + + test("reports a service the catalog does not model", () => { + expect( + planCatalogUpdate({ + source: fixture, + service: "kong", + version: "v3.0.0", + digest: DIGEST_A, + }).kind, + ).toBe("unmodelled-service"); + }); + + test("does not confuse postgres with postgrest", () => { + const plan = planCatalogUpdate({ + source: fixture, + service: "postgrest", + version: "v17.0", + digest: DIGEST_A, + }); + + expect(plan.kind).toBe("updated"); + if (plan.kind !== "updated") return; + expect(plan.previousVersion).toBe("v16.2"); + // postgres keeps both of its own pins. + expect(plan.source).toContain(`"17.6.1.168",`); + expect(plan.source).toContain(`"15.14.1.168":`); + }); + + test("rejects an invalid payload instead of rewriting the catalog", () => { + expect(() => + planCatalogUpdate({ + source: fixture, + service: "auth", + version: "v1.0.0", + digest: "sha256:not-a-digest", + }), + ).toThrow(InvalidPayloadError); + }); +}); + +describe("against the real catalog", () => { + test("every modelled service is addressable and idempotent", () => { + // Derived from the catalog itself so a newly modelled workload is covered + // without editing this list. + const services = [ + ...new Set( + [...realCatalog.matchAll(/native\(\s*"([a-z][a-z0-9-]*)",/g)].map( + (match) => match[1] ?? "", + ), + ), + ]; + expect(services.length).toBeGreaterThan(10); + + for (const service of services) { + const bumped = planCatalogUpdate({ + source: realCatalog, + service, + version: "99.99.99", + digest: DIGEST_A, + }); + // 99.x is a line no service carries, so postgres (multi-line) skips while + // every single-line service bumps. Either way it must be recognised. + expect( + bumped.kind === "updated" || bumped.kind === "unmodelled-release-line", + `${service} was not addressable in the real catalog`, + ).toBe(true); + + if (bumped.kind !== "updated") continue; + expect( + planCatalogUpdate({ + source: bumped.source, + service, + version: "99.99.99", + digest: DIGEST_A, + }).kind, + ).toBe("unchanged"); + } + }); +}); diff --git a/.github/scripts/sync-workload-catalog.ts b/.github/scripts/sync-workload-catalog.ts new file mode 100644 index 0000000000..151b02df42 --- /dev/null +++ b/.github/scripts/sync-workload-catalog.ts @@ -0,0 +1,248 @@ +/** + * Syncs `packages/stack/src/model/WorkloadCatalog.ts` to a slim-services + * release. + * + * `@supabase/stack` pins each workload to an exact slim-services artifact + * release — a version plus the digest of its `ghcr.io/supabase/cli/` + * image — because ADR 0017 makes the artifact release the boundary for service + * startup defaults. Those pins therefore track the slim-services release feed, + * NOT `apps/cli-go/pkg/config/templates/Dockerfile`. Dependabot maintains the + * Dockerfile (which the shipped CLI reads via + * `apps/cli/src/shared/services/dockerfile-images.ts`) and cannot maintain this + * catalog: it resolves registry tags and has no way to emit a `sha256:` index + * digest. + * + * The feed is the `mirror-slim-image` repository_dispatch that slim-services + * already sends this repo for the ECR mirror (see `mirror-slim-image.yml` and + * `docs/design/ecr-mirror-dispatch.md` in supabase/slim-services). This script + * consumes the same payload and rewrites the matching catalog entry; + * `sync-stack-workload-catalog.yml` runs it and opens the PR. + * + * Only two source values need rewriting per release. `artifactFor` in + * `WorkloadCatalog.ts` derives `releaseTag`, `assetName`, and every download + * URL from `service` + `version`, and `releases` is derived from + * `defaultVersion` + the container image, so updating the `native(...)` + * positional `defaultVersion` and container image is the whole change. + * + * The payload arrives with whatever authority holds the dispatch token, so it + * is revalidated here rather than trusted from the workflow — the patterns + * below are what keep `version` and `digest` from breaking out of the TypeScript + * string literals they are written into. + * + * Run in CI as: + * bun .github/scripts/sync-workload-catalog.ts + * with SLIM_SERVICE / SLIM_VERSION / SLIM_DIGEST set from the payload. + * + * Exit codes: 0 the sync ran (whether or not it changed anything), 1 invalid + * payload or tool failure. A service the catalog does not model, and a release + * line it does not carry, are both successful no-ops — slim-services publishes + * for consumers beyond this catalog. + * + * `planCatalogUpdate` is pure and unit-tested in `sync-workload-catalog.test.ts`; + * `main()` wires up the real filesystem. + */ + +export const CATALOG_PATH = "packages/stack/src/model/WorkloadCatalog.ts"; + +/** Mirrors the payload validation in `mirror-slim-image.yml`. */ +const SERVICE_PATTERN = /^[a-z][a-z0-9-]*$/; +const VERSION_PATTERN = /^[A-Za-z0-9._-]+$/; +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/; + +const SLIM_IMAGE_PREFIX = "ghcr.io/supabase/cli/"; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * The release line a version belongs to: its leading numeric component, with + * any `v` prefix stripped. Used only to disambiguate services that carry + * several supported lines at once (today just postgres, 17.x alongside 15.x). + */ +export function releaseLine(version: string): string { + const withoutPrefix = version.replace(/^[vV]/, ""); + const separator = withoutPrefix.indexOf("."); + return separator === -1 ? withoutPrefix : withoutPrefix.slice(0, separator); +} + +export interface CatalogUpdateInput { + readonly source: string; + readonly service: string; + readonly version: string; + readonly digest: string; +} + +export type CatalogUpdatePlan = + | { + readonly kind: "updated"; + readonly source: string; + readonly previousVersion: string; + /** `default` bumped the entry's defaultVersion; `additional` bumped one of its extra lines. */ + readonly target: "default" | "additional"; + } + | { readonly kind: "unchanged" } + | { readonly kind: "unmodelled-service" } + | { readonly kind: "unmodelled-release-line"; readonly known: ReadonlyArray }; + +export class InvalidPayloadError extends Error {} + +export function validatePayload(input: { + readonly service: string; + readonly version: string; + readonly digest: string; +}): void { + if (!SERVICE_PATTERN.test(input.service)) { + throw new InvalidPayloadError(`invalid service name: '${input.service}'`); + } + if (!VERSION_PATTERN.test(input.version)) { + throw new InvalidPayloadError(`invalid version: '${input.version}'`); + } + if (!DIGEST_PATTERN.test(input.digest)) { + throw new InvalidPayloadError(`invalid digest: '${input.digest}'`); + } +} + +/** + * The `native("", "", ""` positional arguments. The + * image is anchored to this service's own slim repository, so `postgres` cannot + * match `postgrest` and vice versa. + */ +function defaultEntryPattern(service: string): RegExp { + const s = escapeRegExp(service); + return new RegExp( + `(native\\(\\s*"${s}",\\s*")([^"]+)("\\s*,\\s*")(${escapeRegExp(SLIM_IMAGE_PREFIX)}${s}:[^"]+)(")`, + ); +} + +/** + * Entries of an `additionalReleases` map for this service: `"": + * ""`. The `:` between key and value is what separates these from the + * positional arguments above; `\s` spans the line break oxfmt introduces when + * a digest-pinned value wraps. + */ +function additionalEntryPattern(service: string, version?: string): RegExp { + const s = escapeRegExp(service); + const key = version === undefined ? `[^"]+` : escapeRegExp(version); + // Groups: 1 the version key, 2 the key/value separator (preserved so the + // rewrite keeps oxfmt's existing wrapping), 3 the container image. + return new RegExp( + `"(${key})"(\\s*:\\s*)"(${escapeRegExp(SLIM_IMAGE_PREFIX)}${s}:[^"]+)"`, + version === undefined ? "g" : "", + ); +} + +function slimImageRef(service: string, version: string, digest: string): string { + return `${SLIM_IMAGE_PREFIX}${service}:${version}@${digest}`; +} + +/** + * Rewrites the catalog entry for `service` onto `version`/`digest`, or explains + * why there was nothing to do. + */ +export function planCatalogUpdate(input: CatalogUpdateInput): CatalogUpdatePlan { + validatePayload(input); + const { source, service, version, digest } = input; + + const defaultMatch = defaultEntryPattern(service).exec(source); + if (defaultMatch === null) { + return { kind: "unmodelled-service" }; + } + + const currentDefaultVersion = defaultMatch[2] ?? ""; + const currentDefaultImage = defaultMatch[4] ?? ""; + const desiredImage = slimImageRef(service, version, digest); + + const additional = [...source.matchAll(additionalEntryPattern(service))].map((match) => ({ + version: match[1] ?? "", + image: match[3] ?? "", + })); + + // With one modelled line there is no ambiguity — every release bumps the + // default. With several (postgres), the release line decides which one moves, + // so a 15.x release can never overwrite the 17.x default. + const bumpsDefault = + additional.length === 0 || releaseLine(version) === releaseLine(currentDefaultVersion); + + if (bumpsDefault) { + if (currentDefaultVersion === version && currentDefaultImage === desiredImage) { + return { kind: "unchanged" }; + } + return { + kind: "updated", + source: source.replace( + defaultEntryPattern(service), + (_full, prefix: string, _version: string, mid: string, _image: string, suffix: string) => + `${prefix}${version}${mid}${desiredImage}${suffix}`, + ), + previousVersion: currentDefaultVersion, + target: "default", + }; + } + + const sameLine = additional.find((entry) => releaseLine(entry.version) === releaseLine(version)); + if (sameLine === undefined) { + return { + kind: "unmodelled-release-line", + known: [currentDefaultVersion, ...additional.map((entry) => entry.version)], + }; + } + + if (sameLine.version === version && sameLine.image === desiredImage) { + return { kind: "unchanged" }; + } + + return { + kind: "updated", + source: source.replace( + additionalEntryPattern(service, sameLine.version), + (_full, _key: string, separator: string) => `"${version}"${separator}"${desiredImage}"`, + ), + previousVersion: sameLine.version, + target: "additional", + }; +} + +function requireEnv(name: string): string { + const value = process.env[name]; + if (value === undefined || value.trim() === "") { + throw new InvalidPayloadError(`missing required environment variable: ${name}`); + } + return value.trim(); +} + +async function main(): Promise { + const service = requireEnv("SLIM_SERVICE"); + const version = requireEnv("SLIM_VERSION"); + const digest = requireEnv("SLIM_DIGEST"); + + const source = await Bun.file(CATALOG_PATH).text(); + const plan = planCatalogUpdate({ source, service, version, digest }); + + switch (plan.kind) { + case "unmodelled-service": + console.log(`::notice ::${CATALOG_PATH} models no '${service}' workload; nothing to sync.`); + return; + case "unmodelled-release-line": + console.log( + `::notice ::${service} ${version} is not on a release line ${CATALOG_PATH} carries (${plan.known.join(", ")}); nothing to sync.`, + ); + return; + case "unchanged": + console.log(`::notice ::${service} is already pinned to ${version} at ${digest}.`); + return; + case "updated": + await Bun.write(CATALOG_PATH, plan.source); + console.log( + `Updated ${service} ${plan.target} release ${plan.previousVersion} -> ${version} (${digest}).`, + ); + return; + } +} + +if (import.meta.main) { + main().catch((error: unknown) => { + console.log(`::error ::${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + }); +} diff --git a/.github/workflows/sync-stack-workload-catalog.yml b/.github/workflows/sync-stack-workload-catalog.yml new file mode 100644 index 0000000000..a7091ee1c8 --- /dev/null +++ b/.github/workflows/sync-stack-workload-catalog.yml @@ -0,0 +1,138 @@ +name: Sync Stack Workload Catalog + +# Keeps `packages/stack/src/model/WorkloadCatalog.ts` on the slim-services +# release feed. +# +# `@supabase/stack` pins each workload to an exact slim-services artifact +# release — a version plus its `ghcr.io/supabase/cli/` image digest — +# because ADR 0017 makes the artifact release the boundary for service startup +# defaults. Dependabot maintains +# `apps/cli-go/pkg/config/templates/Dockerfile` (the manifest the shipped CLI +# reads) and structurally cannot maintain this catalog: it resolves registry +# tags and never produces a `sha256:` index digest. So this catalog rides the +# same `mirror-slim-image` repository_dispatch that slim-services already sends +# for the ECR mirror, which carries exactly the service/version/digest triple +# the catalog needs. +# +# This is deliberately a separate workflow from `mirror-slim-image.yml` rather +# than another job in it: that mirror runs against the sender's 15-minute +# verification poll, and a catalog PR failing must never delay it or turn its +# run red. Both workflows subscribe to the same dispatch type and run +# independently. +# +# Opens a PR rather than pushing: a service version pin is a reviewable change +# and `develop` is protected. Re-dispatches are no-ops (the script exits +# without touching the file when the pin already matches), so the sender's +# retry path does not produce duplicate PRs. + +on: + repository_dispatch: + types: + - mirror-slim-image + workflow_dispatch: + inputs: + service: + description: "Service name (e.g. postgrest)" + required: true + type: string + version: + description: "Image tag (e.g. v16.2)" + required: true + type: string + digest: + description: "Expected index digest (sha256:<64 hex chars>)" + required: true + type: string + +permissions: + contents: read + +concurrency: + # Serialised per service so two releases of the same service cannot race each + # other onto the shared branch below. Not cancel-in-progress: a superseded run + # may already have opened the PR. + group: sync-stack-workload-catalog-${{ github.event.client_payload.service || inputs.service }} + cancel-in-progress: false + +jobs: + sync: + name: Sync workload catalog + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + with: + dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + + # The payload arrives with whatever authority holds the dispatch token, so + # the script revalidates service/version/digest itself and exits 1 on + # anything that could break out of the TypeScript string literals it + # writes into. Values are passed through env, never interpolated into the + # shell. + - name: Sync workload catalog + env: + SLIM_SERVICE: ${{ github.event.client_payload.service || inputs.service }} + SLIM_VERSION: ${{ github.event.client_payload.version || inputs.version }} + SLIM_DIGEST: ${{ github.event.client_payload.digest || inputs.digest }} + run: bun .github/scripts/sync-workload-catalog.ts + + - name: Format catalog + run: pnpm run fmt:fix + + - name: Check for catalog changes + id: check + run: | + if git diff --exit-code --quiet packages/stack/src/model/WorkloadCatalog.ts; then + echo "Workload catalog is already in sync." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "Workload catalog updated." + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + + # Prove the rewritten pin still compiles and satisfies the catalog's own + # invariants before asking anyone to look at a PR. + - name: Type-check stack + if: steps.check.outputs.has_changes == 'true' + run: pnpm types:check + working-directory: packages/stack + + - name: Test catalog + if: steps.check.outputs.has_changes == 'true' + run: pnpm run test:unit && pnpm run test:integration + working-directory: packages/stack + + - name: Generate token + if: steps.check.outputs.has_changes == 'true' + 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-pull-requests: write + permission-contents: write + + - name: Create Pull Request + if: steps.check.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "chore(stack): pin ${{ github.event.client_payload.service || inputs.service }} to ${{ github.event.client_payload.version || inputs.version }}" + title: "chore(stack): pin ${{ github.event.client_payload.service || inputs.service }} to ${{ github.event.client_payload.version || inputs.version }}" + body: | + `supabase/slim-services` published + `${{ github.event.client_payload.service || inputs.service }}` `${{ github.event.client_payload.version || inputs.version }}`, so + `packages/stack/src/model/WorkloadCatalog.ts` now pins that release and its + image digest. + + Opened automatically from the `mirror-slim-image` dispatch that drives the + ECR mirror — the same event, consumed independently. `artifactFor` derives the + native release tag, asset names, and download URLs from the service and + version, so this pin is the whole change. + branch: sync/stack-workload-catalog-${{ github.event.client_payload.service || inputs.service }} + base: develop diff --git a/packages/stack/src/model/WorkloadCatalog.ts b/packages/stack/src/model/WorkloadCatalog.ts index 20077420ac..2c71228feb 100644 --- a/packages/stack/src/model/WorkloadCatalog.ts +++ b/packages/stack/src/model/WorkloadCatalog.ts @@ -52,7 +52,21 @@ const native = ( containerAlias: options.containerAlias ?? `supabase-${service}`, }); -/** The single authoritative private workload identity table. */ +/** + * The single authoritative private workload identity table. + * + * These pins track the `supabase/slim-services` release feed, NOT + * `apps/cli-go/pkg/config/templates/Dockerfile` — ADR 0017 makes the artifact + * release the boundary for service startup defaults, so a version here carries + * its `ghcr.io/supabase/cli` image digest and moves independently of the + * Dockerfile pin for the same service. The two deliberately diverge; do not + * "reconcile" them. + * + * Maintained by `.github/workflows/sync-stack-workload-catalog.yml`, off the + * `mirror-slim-image` dispatch slim-services sends on each release. Dependabot + * owns the Dockerfile and cannot own this table: it resolves registry tags and + * never produces a `sha256:` digest. + */ const workloadCatalog = { "database:database": native( "postgres", From 3953876f985dad2c2f0c1460bb0626ff6c1f2b7c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 14:21:34 +0000 Subject: [PATCH 3/3] docs(repo): make the workload-catalog sync comments terse Review feedback on #6521: the comments were far too long. Cuts the module doc from 43 lines to 13, the workflow header from 26 to 11, and the WorkloadCatalog provenance note from 15 to 5, plus the inline blocks in all four files. Keeps only what a reader cannot infer from the code: that dependabot cannot own these pins because they carry digests, that the payload is untrusted, and the postgres two-release-line rule. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PWXmTVTiQCwuyjZ8Mfw9NL --- .github/scripts/sync-workload-catalog.test.ts | 8 +- .github/scripts/sync-workload-catalog.ts | 79 ++++--------------- .../workflows/sync-stack-workload-catalog.yml | 61 +++++--------- .../shared/services/slim-images.unit.test.ts | 8 +- packages/stack/src/model/WorkloadCatalog.ts | 14 +--- 5 files changed, 44 insertions(+), 126 deletions(-) diff --git a/.github/scripts/sync-workload-catalog.test.ts b/.github/scripts/sync-workload-catalog.test.ts index 74d88cbc0c..9f4f129795 100644 --- a/.github/scripts/sync-workload-catalog.test.ts +++ b/.github/scripts/sync-workload-catalog.test.ts @@ -11,10 +11,7 @@ import { const DIGEST_A = `sha256:${"a".repeat(64)}`; const DIGEST_B = `sha256:${"b".repeat(64)}`; -/** - * The real catalog, so these tests fail if its shape drifts away from what the - * patterns expect rather than passing against a stale hand-written fixture. - */ +/** The real catalog, so a shape drift fails here instead of passing on a stale fixture. */ const realCatalog = await Bun.file(CATALOG_PATH).text(); /** A trimmed catalog carrying both entry shapes the script has to rewrite. */ @@ -262,8 +259,7 @@ describe("planCatalogUpdate", () => { describe("against the real catalog", () => { test("every modelled service is addressable and idempotent", () => { - // Derived from the catalog itself so a newly modelled workload is covered - // without editing this list. + // Derived from the catalog, so a new workload is covered without editing this. const services = [ ...new Set( [...realCatalog.matchAll(/native\(\s*"([a-z][a-z0-9-]*)",/g)].map( diff --git a/.github/scripts/sync-workload-catalog.ts b/.github/scripts/sync-workload-catalog.ts index 151b02df42..cb00c23bb7 100644 --- a/.github/scripts/sync-workload-catalog.ts +++ b/.github/scripts/sync-workload-catalog.ts @@ -1,45 +1,16 @@ /** - * Syncs `packages/stack/src/model/WorkloadCatalog.ts` to a slim-services - * release. + * Pins one workload in `packages/stack/src/model/WorkloadCatalog.ts` to a + * slim-services release, driven by the same `mirror-slim-image` dispatch as the + * ECR mirror (`mirror-slim-image.yml`). * - * `@supabase/stack` pins each workload to an exact slim-services artifact - * release — a version plus the digest of its `ghcr.io/supabase/cli/` - * image — because ADR 0017 makes the artifact release the boundary for service - * startup defaults. Those pins therefore track the slim-services release feed, - * NOT `apps/cli-go/pkg/config/templates/Dockerfile`. Dependabot maintains the - * Dockerfile (which the shipped CLI reads via - * `apps/cli/src/shared/services/dockerfile-images.ts`) and cannot maintain this - * catalog: it resolves registry tags and has no way to emit a `sha256:` index - * digest. + * Dependabot owns the Dockerfile and cannot own this table: these pins carry + * image digests, which tag resolution never produces (ADR 0017). The dispatch + * payload is untrusted and revalidated here — those patterns are what keep + * `version`/`digest` inside the string literals they are written into. * - * The feed is the `mirror-slim-image` repository_dispatch that slim-services - * already sends this repo for the ECR mirror (see `mirror-slim-image.yml` and - * `docs/design/ecr-mirror-dispatch.md` in supabase/slim-services). This script - * consumes the same payload and rewrites the matching catalog entry; - * `sync-stack-workload-catalog.yml` runs it and opens the PR. - * - * Only two source values need rewriting per release. `artifactFor` in - * `WorkloadCatalog.ts` derives `releaseTag`, `assetName`, and every download - * URL from `service` + `version`, and `releases` is derived from - * `defaultVersion` + the container image, so updating the `native(...)` - * positional `defaultVersion` and container image is the whole change. - * - * The payload arrives with whatever authority holds the dispatch token, so it - * is revalidated here rather than trusted from the workflow — the patterns - * below are what keep `version` and `digest` from breaking out of the TypeScript - * string literals they are written into. - * - * Run in CI as: - * bun .github/scripts/sync-workload-catalog.ts - * with SLIM_SERVICE / SLIM_VERSION / SLIM_DIGEST set from the payload. - * - * Exit codes: 0 the sync ran (whether or not it changed anything), 1 invalid - * payload or tool failure. A service the catalog does not model, and a release - * line it does not carry, are both successful no-ops — slim-services publishes - * for consumers beyond this catalog. - * - * `planCatalogUpdate` is pure and unit-tested in `sync-workload-catalog.test.ts`; - * `main()` wires up the real filesystem. + * Run: `bun .github/scripts/sync-workload-catalog.ts` with SLIM_SERVICE, + * SLIM_VERSION, SLIM_DIGEST. Exit 1 on an invalid payload; an unmodelled + * service or release line is a successful no-op. */ export const CATALOG_PATH = "packages/stack/src/model/WorkloadCatalog.ts"; @@ -55,11 +26,7 @@ function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -/** - * The release line a version belongs to: its leading numeric component, with - * any `v` prefix stripped. Used only to disambiguate services that carry - * several supported lines at once (today just postgres, 17.x alongside 15.x). - */ +/** Leading numeric component, `v` stripped. Only postgres carries >1 line. */ export function releaseLine(version: string): string { const withoutPrefix = version.replace(/^[vV]/, ""); const separator = withoutPrefix.indexOf("."); @@ -103,11 +70,7 @@ export function validatePayload(input: { } } -/** - * The `native("", "", ""` positional arguments. The - * image is anchored to this service's own slim repository, so `postgres` cannot - * match `postgrest` and vice versa. - */ +/** `native("", "", ""` — image anchored so postgres != postgrest. */ function defaultEntryPattern(service: string): RegExp { const s = escapeRegExp(service); return new RegExp( @@ -115,17 +78,11 @@ function defaultEntryPattern(service: string): RegExp { ); } -/** - * Entries of an `additionalReleases` map for this service: `"": - * ""`. The `:` between key and value is what separates these from the - * positional arguments above; `\s` spans the line break oxfmt introduces when - * a digest-pinned value wraps. - */ +/** `additionalReleases` entries: `"": ""`. The `:` is what distinguishes them. */ function additionalEntryPattern(service: string, version?: string): RegExp { const s = escapeRegExp(service); const key = version === undefined ? `[^"]+` : escapeRegExp(version); - // Groups: 1 the version key, 2 the key/value separator (preserved so the - // rewrite keeps oxfmt's existing wrapping), 3 the container image. + // Groups: 1 key, 2 separator (kept, to preserve wrapping), 3 image. return new RegExp( `"(${key})"(\\s*:\\s*)"(${escapeRegExp(SLIM_IMAGE_PREFIX)}${s}:[^"]+)"`, version === undefined ? "g" : "", @@ -136,10 +93,7 @@ function slimImageRef(service: string, version: string, digest: string): string return `${SLIM_IMAGE_PREFIX}${service}:${version}@${digest}`; } -/** - * Rewrites the catalog entry for `service` onto `version`/`digest`, or explains - * why there was nothing to do. - */ +/** Rewrites `service`'s entry onto `version`/`digest`, or says why there was nothing to do. */ export function planCatalogUpdate(input: CatalogUpdateInput): CatalogUpdatePlan { validatePayload(input); const { source, service, version, digest } = input; @@ -158,8 +112,7 @@ export function planCatalogUpdate(input: CatalogUpdateInput): CatalogUpdatePlan image: match[3] ?? "", })); - // With one modelled line there is no ambiguity — every release bumps the - // default. With several (postgres), the release line decides which one moves, + // One line: always the default. Several (postgres): the release line picks, // so a 15.x release can never overwrite the 17.x default. const bumpsDefault = additional.length === 0 || releaseLine(version) === releaseLine(currentDefaultVersion); diff --git a/.github/workflows/sync-stack-workload-catalog.yml b/.github/workflows/sync-stack-workload-catalog.yml index a7091ee1c8..4ff9f7df1c 100644 --- a/.github/workflows/sync-stack-workload-catalog.yml +++ b/.github/workflows/sync-stack-workload-catalog.yml @@ -1,29 +1,15 @@ name: Sync Stack Workload Catalog -# Keeps `packages/stack/src/model/WorkloadCatalog.ts` on the slim-services -# release feed. +# Pins `packages/stack/src/model/WorkloadCatalog.ts` to slim-services releases, +# off the same `mirror-slim-image` dispatch that drives the ECR mirror. +# Dependabot owns the Dockerfile and cannot own this table: these pins carry +# image digests, which tag resolution never produces (ADR 0017). # -# `@supabase/stack` pins each workload to an exact slim-services artifact -# release — a version plus its `ghcr.io/supabase/cli/` image digest — -# because ADR 0017 makes the artifact release the boundary for service startup -# defaults. Dependabot maintains -# `apps/cli-go/pkg/config/templates/Dockerfile` (the manifest the shipped CLI -# reads) and structurally cannot maintain this catalog: it resolves registry -# tags and never produces a `sha256:` index digest. So this catalog rides the -# same `mirror-slim-image` repository_dispatch that slim-services already sends -# for the ECR mirror, which carries exactly the service/version/digest triple -# the catalog needs. -# -# This is deliberately a separate workflow from `mirror-slim-image.yml` rather -# than another job in it: that mirror runs against the sender's 15-minute -# verification poll, and a catalog PR failing must never delay it or turn its -# run red. Both workflows subscribe to the same dispatch type and run -# independently. -# -# Opens a PR rather than pushing: a service version pin is a reviewable change -# and `develop` is protected. Re-dispatches are no-ops (the script exits -# without touching the file when the pin already matches), so the sender's -# retry path does not produce duplicate PRs. +# Separate from `mirror-slim-image.yml` on purpose — that mirror runs against +# the sender's 15-minute verification poll and must not be delayed or reddened +# by a catalog PR. Opens a PR rather than pushing: `develop` is protected and a +# version pin is reviewable. Re-dispatches are no-ops, so retries do not open +# duplicate PRs. on: repository_dispatch: @@ -48,9 +34,8 @@ permissions: contents: read concurrency: - # Serialised per service so two releases of the same service cannot race each - # other onto the shared branch below. Not cancel-in-progress: a superseded run - # may already have opened the PR. + # Per service, so two releases of one service cannot race onto the shared + # branch. Not cancel-in-progress: a superseded run may already have opened it. group: sync-stack-workload-catalog-${{ github.event.client_payload.service || inputs.service }} cancel-in-progress: false @@ -69,11 +54,8 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - # The payload arrives with whatever authority holds the dispatch token, so - # the script revalidates service/version/digest itself and exits 1 on - # anything that could break out of the TypeScript string literals it - # writes into. Values are passed through env, never interpolated into the - # shell. + # Payload is untrusted; the script revalidates it. Passed via env, never + # interpolated into the shell. - name: Sync workload catalog env: SLIM_SERVICE: ${{ github.event.client_payload.service || inputs.service }} @@ -95,8 +77,7 @@ jobs: echo "has_changes=true" >> "$GITHUB_OUTPUT" fi - # Prove the rewritten pin still compiles and satisfies the catalog's own - # invariants before asking anyone to look at a PR. + # Prove the rewritten pin compiles and passes the catalog's own tests. - name: Type-check stack if: steps.check.outputs.has_changes == 'true' run: pnpm types:check @@ -125,14 +106,12 @@ jobs: commit-message: "chore(stack): pin ${{ github.event.client_payload.service || inputs.service }} to ${{ github.event.client_payload.version || inputs.version }}" title: "chore(stack): pin ${{ github.event.client_payload.service || inputs.service }} to ${{ github.event.client_payload.version || inputs.version }}" body: | - `supabase/slim-services` published - `${{ github.event.client_payload.service || inputs.service }}` `${{ github.event.client_payload.version || inputs.version }}`, so - `packages/stack/src/model/WorkloadCatalog.ts` now pins that release and its - image digest. + Pins `packages/stack/src/model/WorkloadCatalog.ts` to the + `${{ github.event.client_payload.service || inputs.service }}` `${{ github.event.client_payload.version || inputs.version }}` + slim-services release and its image digest. - Opened automatically from the `mirror-slim-image` dispatch that drives the - ECR mirror — the same event, consumed independently. `artifactFor` derives the - native release tag, asset names, and download URLs from the service and - version, so this pin is the whole change. + Opened from the `mirror-slim-image` dispatch. `artifactFor` derives the + native release tag, asset names, and URLs from service + version, so this + pin is the whole change. branch: sync/stack-workload-catalog-${{ github.event.client_payload.service || inputs.service }} base: develop diff --git a/apps/cli/src/shared/services/slim-images.unit.test.ts b/apps/cli/src/shared/services/slim-images.unit.test.ts index 54f8921b40..2f1e8de860 100644 --- a/apps/cli/src/shared/services/slim-images.unit.test.ts +++ b/apps/cli/src/shared/services/slim-images.unit.test.ts @@ -43,11 +43,9 @@ describe("toSlimImage", () => { ); }); - // Tag-scheme assertions use fixed pins rather than the Dockerfile manifest: - // dependabot bumps that manifest, and an expectation spelling out the current - // pin would turn every bump into a failing test. The `it.each` above keeps the - // live manifest covered for the part that must track it — the repository each - // alias maps to. + // Fixed pins, not manifest pins: dependabot bumps the manifest, so spelling + // out a current pin here would fail on every bump. The `it.each` above covers + // the part that must track it (the repository each alias maps to). it("keeps a single v on pins already prefixed on docker.io", () => { expect(toSlimImage("realtime", "supabase/realtime:v2.130.0")).toBe( "ghcr.io/supabase/cli/realtime:v2.130.0", diff --git a/packages/stack/src/model/WorkloadCatalog.ts b/packages/stack/src/model/WorkloadCatalog.ts index 2c71228feb..4a3e880524 100644 --- a/packages/stack/src/model/WorkloadCatalog.ts +++ b/packages/stack/src/model/WorkloadCatalog.ts @@ -55,17 +55,9 @@ const native = ( /** * The single authoritative private workload identity table. * - * These pins track the `supabase/slim-services` release feed, NOT - * `apps/cli-go/pkg/config/templates/Dockerfile` — ADR 0017 makes the artifact - * release the boundary for service startup defaults, so a version here carries - * its `ghcr.io/supabase/cli` image digest and moves independently of the - * Dockerfile pin for the same service. The two deliberately diverge; do not - * "reconcile" them. - * - * Maintained by `.github/workflows/sync-stack-workload-catalog.yml`, off the - * `mirror-slim-image` dispatch slim-services sends on each release. Dependabot - * owns the Dockerfile and cannot own this table: it resolves registry tags and - * never produces a `sha256:` digest. + * Pins track the slim-services release feed (ADR 0017), not the Dockerfile, and + * deliberately diverge from it — do not "reconcile" the two. Maintained by + * `.github/workflows/sync-stack-workload-catalog.yml`. */ const workloadCatalog = { "database:database": native(