Skip to content
Open
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
212 changes: 212 additions & 0 deletions .github/workflows/mirror-slim-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
name: Mirror Slim Image

# Mirrors slim service images published by supabase/slim-services from
# ghcr.io/supabase/cli/<service>:<version> to
# public.ecr.aws/supabase/cli/<service>:<version>.
#
# The slim-services release pipeline sends a `mirror-slim-image`
# repository_dispatch to this repo, then anonymously polls the ECR Public
# destination (15-minute timeout) and fails its release unless the destination
# resolves to the exact index digest it published. The copy must therefore be
# digest-preserving: we use `regctl image copy`, which moves the whole OCI
# index (all platform manifests and referrers) byte-for-byte. Do NOT switch
# this to `docker buildx imagetools create` — it can rewrite the index and
# change its digest, breaking the sender's verification.
#
# The payload arrives with whatever authority holds the dispatch token, so it
# is validated as untrusted input: names are pattern-checked, source and
# destination are derived here rather than trusted from the payload, and the
# source must resolve to the digest claimed by the sender before anything is
# copied.
#
# Full contract: docs/design/ecr-mirror-dispatch.md in supabase/slim-services.

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:
group: mirror-slim-image-${{ github.event.client_payload.service || inputs.service }}-${{ github.event.client_payload.version || inputs.version }}
cancel-in-progress: false

jobs:
mirror:
runs-on: ubuntu-latest
# The sender's poll times out after 15 minutes; fail fast instead of
# hanging past that window.
timeout-minutes: 10
permissions:
contents: read
packages: read
id-token: write
steps:
- name: Validate payload
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
SERVICE: ${{ github.event.client_payload.service || inputs.service }}
VERSION: ${{ github.event.client_payload.version || inputs.version }}
DIGEST: ${{ github.event.client_payload.digest || inputs.digest }}
PAYLOAD_SOURCE: ${{ github.event.client_payload.source }}
PAYLOAD_DESTINATION: ${{ github.event.client_payload.destination }}
run: |
set -euo pipefail
if [[ ! "$SERVICE" =~ ^[a-z][a-z0-9-]*$ ]]; then
echo "::error::invalid service name: '$SERVICE'"
exit 1
fi
if [[ ! "$VERSION" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::error::invalid version: '$VERSION'"
exit 1
fi
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "::error::invalid digest: '$DIGEST'"
exit 1
fi
SOURCE="ghcr.io/supabase/cli/${SERVICE}:${VERSION}"
DESTINATION="public.ecr.aws/supabase/cli/${SERVICE}:${VERSION}"
# Never trust the payload's source/destination strings; require them
# to match the values derived from service + version.
if [ "$EVENT_NAME" = "repository_dispatch" ]; then
if [ "$PAYLOAD_SOURCE" != "$SOURCE" ]; then
echo "::error::payload source '$PAYLOAD_SOURCE' does not match derived '$SOURCE'"
exit 1
fi
if [ "$PAYLOAD_DESTINATION" != "$DESTINATION" ]; then
echo "::error::payload destination '$PAYLOAD_DESTINATION' does not match derived '$DESTINATION'"
exit 1
fi
fi
{
echo "service=$SERVICE"
echo "source=$SOURCE"
echo "destination=$DESTINATION"
echo "digest=$DIGEST"
} >> "$GITHUB_OUTPUT"

- name: Install regctl
# Installed under $RUNNER_TEMP (always writable by the job user) and
# exposed to later steps via $GITHUB_PATH.
run: |
set -euo pipefail
install -d "${RUNNER_TEMP}/regctl-bin"
curl -fsSLo "${RUNNER_TEMP}/regctl-bin/regctl" \
https://github.com/regclient/regclient/releases/download/v0.11.5/regctl-linux-amd64
echo "c93aa7638749f5aaac1a8e01787321889c78f0101809bb2880343478d0ba0467 ${RUNNER_TEMP}/regctl-bin/regctl" | sha256sum -c -
chmod +x "${RUNNER_TEMP}/regctl-bin/regctl"
echo "${RUNNER_TEMP}/regctl-bin" >> "$GITHUB_PATH"
"${RUNNER_TEMP}/regctl-bin/regctl" version

- name: Log in to ghcr.io
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Verify source digest
env:
SOURCE: ${{ steps.validate.outputs.source }}
DIGEST: ${{ steps.validate.outputs.digest }}
run: |
set -euo pipefail
SOURCE_DIGEST="$(regctl manifest head "$SOURCE")"
if [ "$SOURCE_DIGEST" != "$DIGEST" ]; then
Comment thread
avallete marked this conversation as resolved.
echo "::error::source $SOURCE resolves to $SOURCE_DIGEST, expected $DIGEST"
exit 1
fi

- name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1

- name: Log in to ECR Public
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: public.ecr.aws

- name: Check if destination is already mirrored
id: check
env:
DESTINATION: ${{ steps.validate.outputs.destination }}
DIGEST: ${{ steps.validate.outputs.digest }}
run: |
set -euo pipefail
if DEST_DIGEST="$(regctl manifest head "$DESTINATION" 2>/dev/null)" \
&& [ "$DEST_DIGEST" = "$DIGEST" ]; then
echo "$DESTINATION already resolves to $DIGEST; nothing to do"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Ensure ECR Public repository exists
if: steps.check.outputs.skip != 'true'
env:
SERVICE: ${{ steps.validate.outputs.service }}
run: |
set -euo pipefail
REPO_NAME="cli/${SERVICE}"
if aws ecr-public describe-repositories \
--repository-names "$REPO_NAME" --region us-east-1 >/dev/null 2>&1; then
echo "ECR Public repository $REPO_NAME exists"
exit 0
fi
if CREATE_ERR="$(aws ecr-public create-repository \
--repository-name "$REPO_NAME" --region us-east-1 2>&1 >/dev/null)"; then
echo "created ECR Public repository $REPO_NAME"
elif grep -q RepositoryAlreadyExistsException <<< "$CREATE_ERR"; then
# Concurrent run for another version of the same new service won
# the creation race; the repository exists, which is all we need.
echo "ECR Public repository $REPO_NAME was created concurrently"
else
echo "$CREATE_ERR"
echo "::error::ECR Public repository '$REPO_NAME' does not exist and this role cannot create it (missing ecr-public:CreateRepository). Create it once manually — aws ecr-public create-repository --repository-name '$REPO_NAME' --region us-east-1 — then re-run this workflow."
exit 1
fi

- name: Mirror image
if: steps.check.outputs.skip != 'true'
env:
SOURCE: ${{ steps.validate.outputs.source }}
DESTINATION: ${{ steps.validate.outputs.destination }}
DIGEST: ${{ steps.validate.outputs.digest }}
# Copy by digest so the copy cannot race a tag move on the source; the
# whole index, all child manifests, and any referrers move as-is.
run: |
set -euo pipefail
regctl image copy --referrers --digest-tags \
"${SOURCE%:*}@${DIGEST}" "$DESTINATION"

- name: Verify destination digest
env:
DESTINATION: ${{ steps.validate.outputs.destination }}
DIGEST: ${{ steps.validate.outputs.digest }}
run: |
set -euo pipefail
DEST_DIGEST="$(regctl manifest head "$DESTINATION")"
if [ "$DEST_DIGEST" != "$DIGEST" ]; then
echo "::error::destination $DESTINATION resolves to $DEST_DIGEST, expected $DIGEST"
exit 1
fi
echo "$DESTINATION resolves to $DIGEST"
Loading