From 4b072949fa3debbc0d9c88c4487a33f85055c234 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 08:32:35 +0000 Subject: [PATCH] ci(repo): mirror slim images to the central registry's ECR Public Slim images now mirror to public.ecr.aws/supabase-registry/cli/, pushed with the `ecr-push-public-cli` role from the central registry account instead of PROD_AWS_ROLE. The registry stack owns the repositories, so the workflow checks the destination exists and points at the enrollment file when it does not, rather than creating it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SNJube4L1c7kHu88o2JXZG --- .github/scripts/mirror-slim-image.test.ts | 51 +++++++---------------- .github/scripts/mirror-slim-image.ts | 33 ++++----------- .github/scripts/slim-mirror-payload.ts | 2 +- .github/workflows/mirror-slim-image.yml | 16 +++---- docs/adr/0026-slim-artifact-mirrors.md | 6 +-- 5 files changed, 35 insertions(+), 73 deletions(-) diff --git a/.github/scripts/mirror-slim-image.test.ts b/.github/scripts/mirror-slim-image.test.ts index fca7fd3810..f19c422986 100644 --- a/.github/scripts/mirror-slim-image.test.ts +++ b/.github/scripts/mirror-slim-image.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test"; import { copyImage, copyNatives, - ensureEcrPublicRepo, + checkEcrPublicRepo, fetchNatives, main, nativesFromEvent, @@ -46,7 +46,7 @@ describe("validateMirrorDispatch", () => { version: "v16.2", digest: DIGEST, source: "ghcr.io/supabase/cli/postgrest:v16.2", - destination: "public.ecr.aws/supabase/cli/postgrest:v16.2", + destination: "public.ecr.aws/supabase-registry/cli/postgrest:v16.2", }); }); @@ -58,7 +58,7 @@ describe("validateMirrorDispatch", () => { version: "v16.2", digest: DIGEST, payloadSource: "ghcr.io/evil/cli/postgrest:v16.2", - payloadDestination: "public.ecr.aws/supabase/cli/postgrest:v16.2", + payloadDestination: "public.ecr.aws/supabase-registry/cli/postgrest:v16.2", }), ).toThrow(InvalidPayloadError); }); @@ -122,11 +122,11 @@ describe("verifyDigest", () => { }); }); -describe("ensureEcrPublicRepo", () => { - test("no-ops when the repository exists", async () => { +describe("checkEcrPublicRepo", () => { + test("passes when the repository exists", async () => { const run: RunCommand = async () => ok(); const logs: string[] = []; - await ensureEcrPublicRepo({ + await checkEcrPublicRepo({ service: "postgrest", run, log: (message) => logs.push(message), @@ -134,34 +134,11 @@ describe("ensureEcrPublicRepo", () => { expect(logs).toEqual(["ECR Public repository cli/postgrest exists"]); }); - test("creates a missing repository", async () => { - const run: RunCommand = async (argv) => - argv.includes("describe-repositories") ? fail("not found") : ok(); - const logs: string[] = []; - await ensureEcrPublicRepo({ - service: "postgrest", - run, - log: (message) => logs.push(message), - }); - expect(logs).toEqual(["created ECR Public repository cli/postgrest"]); - }); - - test("treats a create race as success", async () => { - const run: RunCommand = async () => fail("RepositoryAlreadyExistsException"); - const logs: string[] = []; - await ensureEcrPublicRepo({ - service: "auth", - run, - log: (message) => logs.push(message), - }); - expect(logs).toEqual(["ECR Public repository cli/auth was created concurrently"]); - }); - - test("fails when create is denied", async () => { - const run: RunCommand = async () => fail("AccessDenied"); + test("fails with the enrollment to add when the repository is missing", async () => { + const run: RunCommand = async () => fail("RepositoryNotFoundException"); await expect( - ensureEcrPublicRepo({ service: "postgrest", run, log: () => undefined }), - ).rejects.toThrow(/CreateRepository/); + checkEcrPublicRepo({ service: "postgrest", run, log: () => undefined }), + ).rejects.toThrow(/pulumi\/registry\/public\/cli\/cli\.yaml/); }); }); @@ -174,7 +151,7 @@ describe("copyImage", () => { }; await copyImage({ source: "ghcr.io/supabase/cli/postgrest:v16.2", - destination: "public.ecr.aws/supabase/cli/postgrest:v16.2", + destination: "public.ecr.aws/supabase-registry/cli/postgrest:v16.2", digest: DIGEST, run, }); @@ -186,7 +163,7 @@ describe("copyImage", () => { "--referrers", "--digest-tags", digestReference("ghcr.io/supabase/cli/postgrest:v16.2", DIGEST), - "public.ecr.aws/supabase/cli/postgrest:v16.2", + "public.ecr.aws/supabase-registry/cli/postgrest:v16.2", ], ]); }); @@ -221,7 +198,7 @@ describe("copyNatives", () => { "image", "copy", `ghcr.io/supabase/cli/postgrest@${DIGEST}`, - "public.ecr.aws/supabase/cli/postgrest:v16.2-native-linux-arm64", + "public.ecr.aws/supabase-registry/cli/postgrest:v16.2-native-linux-arm64", ], ["regctl", "manifest", "head", "ghcr.io/supabase/cli/postgrest:v16.2-native-linux-amd64"], ]); @@ -263,7 +240,7 @@ describe("main", () => { }, }); expect(code).toBe(0); - expect(fields["destination"]).toBe("public.ecr.aws/supabase/cli/postgrest:v16.2"); + expect(fields["destination"]).toBe("public.ecr.aws/supabase-registry/cli/postgrest:v16.2"); }); test("copy-natives no-ops without a dispatch payload", async () => { diff --git a/.github/scripts/mirror-slim-image.ts b/.github/scripts/mirror-slim-image.ts index 0398ec0282..bcdfc5c113 100644 --- a/.github/scripts/mirror-slim-image.ts +++ b/.github/scripts/mirror-slim-image.ts @@ -5,7 +5,7 @@ * * bun .github/scripts/mirror-slim-image.ts validate * bun .github/scripts/mirror-slim-image.ts verify-digest - * bun .github/scripts/mirror-slim-image.ts ensure-repo + * bun .github/scripts/mirror-slim-image.ts check-repo * bun .github/scripts/mirror-slim-image.ts copy-image * bun .github/scripts/mirror-slim-image.ts copy-natives * bun .github/scripts/mirror-slim-image.ts fetch-natives @@ -64,7 +64,7 @@ export type MirrorIo = { const usage = `Usage: bun .github/scripts/mirror-slim-image.ts validate bun .github/scripts/mirror-slim-image.ts verify-digest - bun .github/scripts/mirror-slim-image.ts ensure-repo + bun .github/scripts/mirror-slim-image.ts check-repo bun .github/scripts/mirror-slim-image.ts copy-image bun .github/scripts/mirror-slim-image.ts copy-natives bun .github/scripts/mirror-slim-image.ts fetch-natives @@ -114,7 +114,7 @@ export const verifyDigest = async (options: { log(`${options.reference} resolves to ${options.digest}`); }; -export const ensureEcrPublicRepo = async (options: { +export const checkEcrPublicRepo = async (options: { readonly service: string; readonly run: RunCommand; readonly log?: (message: string) => void; @@ -134,27 +134,10 @@ export const ensureEcrPublicRepo = async (options: { log(`ECR Public repository ${repoName} exists`); return; } - const created = await options.run([ - "aws", - "ecr-public", - "create-repository", - "--repository-name", - repoName, - "--region", - "us-east-1", - ]); - if (created.ok) { - log(`created ECR Public repository ${repoName}`); - return; - } - const detail = `${created.stdout}\n${created.stderr}`; - if (detail.includes("RepositoryAlreadyExistsException")) { - log(`ECR Public repository ${repoName} was created concurrently`); - return; - } - if (detail.trim() !== "") log(detail.trim()); + const detail = `${described.stdout}\n${described.stderr}`.trim(); + if (detail !== "") log(detail); throw new InvalidPayloadError( - `ECR Public repository '${repoName}' does not exist and this role cannot create it (missing ecr-public:CreateRepository). Create it once manually — aws ecr-public create-repository --repository-name '${repoName}' --region us-east-1 — then re-run this workflow.`, + `ECR Public repository '${repoName}' is not available to this role. Add '${options.service}' to pulumi/registry/public/cli/cli.yaml in supabase/platform, then re-run this workflow once it deploys.`, ); }; @@ -483,8 +466,8 @@ export const main = async (argv: ReadonlyArray, io: MirrorIo): Promise`), digest-preserving, via `mirror-slim-image.yml` and `.github/scripts/mirror-slim-image.ts`. Publish each slim **native** archive as an OCI artifact on the same two repositories under `:version-native-` (not `:version-linux-*`, which are image platform tags). GitHub Releases remain the human HTTPS copy and `--clobber` on force. +Publish each slim **image** to GHCR and AWS ECR Public (`public.ecr.aws/supabase-registry/cli/`), digest-preserving, via `mirror-slim-image.yml` and `.github/scripts/mirror-slim-image.ts`. Publish each slim **native** archive as an OCI artifact on the same two repositories under `:version-native-` (not `:version-linux-*`, which are image platform tags). GitHub Releases remain the human HTTPS copy and `--clobber` on force. `force=true` always requests the ECR copy, including when the image digest is unchanged (natives may have moved). The **image** destination digest gates `publish-release` once the dispatch token exists. Native ECR copy is best-effort (`continue-on-error`) and must not fail that release. Catalog sync consumes the image `service` / `version` / `digest` only; native tags ride in a separate `natives[]` payload field. Do not prune untagged GHCR or ECR manifests: already-shipped CLIs still pin old image digests until a catalog PR ships. Natives follow the moved tag immediately. -ECR Public tags are always mutable; `ecr-public create-repository` has no immutability flag. Reuse the existing `PROD_AWS_ROLE` in `supabase/cli`. Do not add a public S3 bucket or a second cloud vendor for this cut. +ECR Public tags are always mutable; `ecr-public create-repository` has no immutability flag. The repositories and the `ecr-push-public-cli` push role live in the central registry account, enrolled in `pulumi/registry/public/cli/cli.yaml` in `supabase/platform`; adding a service means adding it there first. Do not add a public S3 bucket or a second cloud vendor for this cut. Client fail-through (ordered GHCR / ECR / GitHub candidates in the CLI and stack) is out of scope here and lands in a follow-up PR. @@ -27,7 +27,7 @@ Client fail-through (ordered GHCR / ECR / GitHub candidates in the CLI and stack ## Rationale -ECR Public mirroring already exists for other CLI images and needs no new vendor. Native OCI on those same repos reuses `regctl` and that role. Extracting the workflow into bun scripts lets the copy, digest check, and native payload validation run in CI and locally. +ECR Public mirroring already exists for other CLI images and needs no new vendor. Native OCI on those same repos reuses `regctl` and the same push role. Extracting the workflow into bun scripts lets the copy, digest check, and native payload validation run in CI and locally. ## Consequences