Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
/.github/scripts/ai-review/** @supabase/cli
/.github/ai-review/** @supabase/cli

# The cli-artifacts infra workflow deploys with account-admin credentials from develop;
# keep it under maintainer review rather than the ownerless workflow-files rule above.
/.github/workflows/cli-artifacts-infra.yml @supabase/cli

# Generated code. These ownerless rules override the catch-all above so
# CI-green sync PRs (e.g. Management API OpenAPI spec) can be auto-merged.
/apps/cli-go/pkg/api/*.gen.go
Expand Down
116 changes: 116 additions & 0 deletions .github/workflows/cli-artifacts-infra.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: CLI Artifacts Infra

# Deploys infra/cli-artifacts/template.yaml into the cli-artifacts-prod AWS account: the
# public-read S3 bucket that mirrors slim native artifacts and the PutObject-only role the
# mirror-slim-image workflow assumes. Only develop can assume github-deploy in that account, so
# pull requests lint the template and never touch AWS. Details: infra/cli-artifacts/README.md.

on:
pull_request:
paths:
- infra/cli-artifacts/**
- .github/workflows/cli-artifacts-infra.yml
push:
branches:
- develop
paths:
- infra/cli-artifacts/**
- .github/workflows/cli-artifacts-infra.yml
workflow_dispatch:

permissions: {}

# Lint runs are per pull request or ref; deploys serialize on their own group below.
concurrency:
group: cli-artifacts-infra-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install cfn-lint
run: |
set -euo pipefail
python3 -m pip install --user --quiet "cfn-lint==1.57.0"
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"

- name: Lint template
run: cfn-lint infra/cli-artifacts/template.yaml

deploy:
needs: lint
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' && vars.CLI_ARTIFACTS_AWS_ACCOUNT_ID != ''
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: cli-artifacts-infra-deploy
cancel-in-progress: false
permissions:
contents: read
id-token: write
env:
# Passed explicitly on every deploy: `cloudformation deploy` keeps an existing stack's
# previous parameter values unless they are overridden, so template defaults alone
# would never update a live stack.
BUCKET_NAME: supabase-cli-artifacts
TRUSTED_SUBJECT: repo:supabase/cli:ref:refs/heads/develop
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: arn:aws:iam::${{ vars.CLI_ARTIFACTS_AWS_ACCOUNT_ID }}:role/github-deploy
aws-region: us-east-1

- name: Clear a failed initial create
# A create that rolled back leaves the stack in ROLLBACK_COMPLETE, which cannot be
# updated; it holds no resources, so deleting it is the only way to retry.
run: |
set -euo pipefail
status="$(aws cloudformation describe-stacks --stack-name cli-artifacts \
--query "Stacks[0].StackStatus" --output text 2>/dev/null || echo NONE)"
echo "current stack status: ${status}"
if [ "${status}" = "ROLLBACK_COMPLETE" ]; then
aws cloudformation delete-stack --stack-name cli-artifacts
aws cloudformation wait stack-delete-complete --stack-name cli-artifacts
fi

- name: Deploy stack
run: |
set -euo pipefail
aws cloudformation deploy \
--stack-name cli-artifacts \
--template-file infra/cli-artifacts/template.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides \
"BucketName=${BUCKET_NAME}" \
"TrustedSubject=${TRUSTED_SUBJECT}"

- name: Show stack events on failure
if: failure()
run: |
set -euo pipefail
aws cloudformation describe-stack-events \
--stack-name cli-artifacts \
--query "StackEvents[?contains(ResourceStatus, 'FAILED')].[LogicalResourceId,ResourceStatus,ResourceStatusReason]" \
--output table || true

- name: Show stack outputs
run: |
set -euo pipefail
aws cloudformation describe-stacks \
--stack-name cli-artifacts \
--query "Stacks[0].Outputs" \
--output table
Comment thread
avallete marked this conversation as resolved.
54 changes: 54 additions & 0 deletions infra/cli-artifacts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# cli-artifacts

CloudFormation stack for the `cli-artifacts-prod` AWS account (declared in
`supabase/aws-org-root`). It owns the public-read S3 mirror of slim native artifacts that the
CLI downloads when registry blob CDNs or GitHub release assets are blocked, for example inside
agent sandboxes that allow `*.amazonaws.com`.

## What the stack owns

- `AWS::S3::Bucket` `supabase-cli-artifacts`: anonymous `GetObject` through the bucket policy,
TLS required, no `ListBucket`, ACLs blocked, versioning with 30-day noncurrent expiry so a bad
same-version overwrite can be recovered.
- `AWS::IAM::Role` `slim-artifacts-publisher`: trusts the account's GitHub OIDC provider for
`repo:supabase/cli:ref:refs/heads/develop` only (exact match) and can do nothing but
`s3:PutObject` and `s3:AbortMultipartUpload` on the bucket's objects. The slim native mirror job in `.github/workflows/mirror-slim-image.yml`
assumes it per release once that upload step ships.

Objects are addressed as

```
https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com/<service>/<version>/<service>-<version>-<target>.tar.zst
https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com/<service>/<version>/<service>-<version>-<target>.manifest.json
https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com/<service>/<version>/<service>-<version>-<target>.SHA256SUMS
```

which keeps the GitHub release asset names, so the `SHA256SUMS` lines match the archive name
without rewriting.

## How it deploys

`.github/workflows/cli-artifacts-infra.yml` runs `cfn-lint` on pull requests that touch this
directory or the workflow, and deploys the stack on pushes to `develop` with the account's
`github-deploy` role. Every other ref can only
assume the read-only `github-preview` role, which cannot create change sets, so pull requests do
not preview against AWS. The deploy job is skipped until the `CLI_ARTIFACTS_AWS_ACCOUNT_ID`
repository variable is set.

Two roles on purpose: `github-deploy` is account admin and only ever applies this template;
the per-release upload runs as the bucket-scoped publisher role.

The trust is branch-scoped, not workflow-scoped: any job on `develop` with `id-token: write`
can assume the publisher role. IAM evaluates only the `sub` and `aud` claims of the GitHub
token, and narrowing `sub` to one workflow would require a repository-wide subject template
that the org-managed `github-deploy` and `github-preview` trusts do not accept. The controls
are the protected branch, the write-only permission set, the one-hour session cap, and bucket
versioning.

## Changing the trusted branch

Change `TRUSTED_SUBJECT` in the deploy job of the workflow (for example to a release branch)
and merge; the workflow passes it as the `TrustedSubject` parameter on every deploy. The
subject must be the plain `repo:<owner>/<repo>:ref:refs/heads/<branch>` form that
`supabase/cli` currently emits; repositories created or renamed after 2026-07-15 emit immutable
`owner@<id>/repo@<id>` subjects instead.
111 changes: 111 additions & 0 deletions infra/cli-artifacts/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Public-read S3 mirror for slim native artifacts consumed by the Supabase CLI, plus the
bucket-scoped publisher role assumed by the mirror-slim-image workflow.

Parameters:
BucketName:
Type: String
Default: supabase-cli-artifacts
Description: >-
Globally unique bucket name; also the host label in the public URL, so dots are not
allowed (they break TLS on the virtual-hosted endpoint).
AllowedPattern: "^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$"
TrustedSubject:
Type: String
Default: repo:supabase/cli:ref:refs/heads/develop
Description: Exact GitHub Actions OIDC subject allowed to assume the publisher role.
AllowedPattern: "^repo:[A-Za-z0-9._@-]+/[A-Za-z0-9._@-]+:ref:refs/heads/[A-Za-z0-9._/-]+$"

Resources:
ArtifactBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
# Objects are meant to be public through the bucket policy; only ACL-based public
# access stays blocked. Listing is never granted, so keys must be known in advance.
Properties:
BucketName: !Ref BucketName
Comment thread
avallete marked this conversation as resolved.
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: false
RestrictPublicBuckets: false
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: expire-noncurrent-and-abort-multipart
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 30
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7

ArtifactBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ArtifactBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: PublicReadObjects
Effect: Allow
Principal: "*"
Action: s3:GetObject
Resource: !Sub "${ArtifactBucket.Arn}/*"
Comment thread
avallete marked this conversation as resolved.
- Sid: DenyInsecureTransport
Effect: Deny
Principal: "*"
Action: s3:*
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub "${ArtifactBucket.Arn}/*"
Condition:
Bool:
aws:SecureTransport: "false"

PublisherRole:
Type: AWS::IAM::Role
Properties:
RoleName: slim-artifacts-publisher
Description: Uploads slim native artifacts from the supabase/cli mirror workflow.
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
token.actions.githubusercontent.com:sub: !Ref TrustedSubject
Comment thread
avallete marked this conversation as resolved.
Policies:
- PolicyName: put-artifact-objects
PolicyDocument:
Version: "2012-10-17"
Statement:
# AbortMultipartUpload lets a failed `aws s3 cp` clean up its own parts; it
# grants no read, list, or delete on completed objects.
- Effect: Allow
Action:
- s3:PutObject
Comment thread
avallete marked this conversation as resolved.
- s3:AbortMultipartUpload
Resource: !Sub "${ArtifactBucket.Arn}/*"
Comment thread
avallete marked this conversation as resolved.

Outputs:
BucketName:
Value: !Ref ArtifactBucket
BucketUrl:
Value: !Sub "https://${ArtifactBucket}.s3.${AWS::Region}.amazonaws.com"
PublisherRoleArn:
Value: !GetAtt PublisherRole.Arn
Loading