From fedb4d494f9798c78042a7bc130865172a1e944d Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Mon, 21 Sep 2026 15:51:42 +0000 Subject: [PATCH 1/4] ci(repo): add the cli-artifacts S3 bucket and publisher role stack CloudFormation stack for the cli-artifacts-prod account: a public-read bucket for slim native artifacts (anonymous GetObject only, TLS required, no listing, versioning with 30-day noncurrent expiry) and a slim-artifacts-publisher role trusted for develop with s3:PutObject on that bucket's objects and nothing else. The workflow lints the template on pull requests and deploys it from develop with the account's github-deploy role; the deploy job is skipped until CLI_ARTIFACTS_AWS_ACCOUNT_ID is set. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PeXSyQtKtGuwrdNW7uVKom --- .github/workflows/cli-artifacts-infra.yml | 80 +++++++++++++++++ infra/cli-artifacts/README.md | 45 ++++++++++ infra/cli-artifacts/template.yaml | 103 ++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 .github/workflows/cli-artifacts-infra.yml create mode 100644 infra/cli-artifacts/README.md create mode 100644 infra/cli-artifacts/template.yaml diff --git a/.github/workflows/cli-artifacts-infra.yml b/.github/workflows/cli-artifacts-infra.yml new file mode 100644 index 0000000000..49f02188c3 --- /dev/null +++ b/.github/workflows/cli-artifacts-infra.yml @@ -0,0 +1,80 @@ +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: {} + +concurrency: + group: cli-artifacts-infra + cancel-in-progress: false + +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 + permissions: + contents: read + id-token: write + 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: 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 + + - name: Show stack outputs + run: | + aws cloudformation describe-stacks \ + --stack-name cli-artifacts \ + --query "Stacks[0].Outputs" \ + --output table diff --git a/infra/cli-artifacts/README.md b/infra/cli-artifacts/README.md new file mode 100644 index 0000000000..3905192f1f --- /dev/null +++ b/infra/cli-artifacts/README.md @@ -0,0 +1,45 @@ +# 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 and can do nothing but `s3:PutObject` 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///--.tar.zst +https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com///--.manifest.json +https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com///--.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 every pull request 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. + +## Changing the trusted branch + +Redeploy with a different `TrustedSubject` parameter (for example a release branch). The +subject must be the plain `repo:/:ref:refs/heads/` form that +`supabase/cli` currently emits; repositories created or renamed after 2026-07-15 emit immutable +`owner@/repo@` subjects instead. diff --git a/infra/cli-artifacts/template.yaml b/infra/cli-artifacts/template.yaml new file mode 100644 index 0000000000..4368af2522 --- /dev/null +++ b/infra/cli-artifacts/template.yaml @@ -0,0 +1,103 @@ +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. + AllowedPattern: "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$" + TrustedSubject: + Type: String + Default: repo:supabase/cli:ref:refs/heads/develop + Description: GitHub Actions OIDC subject allowed to assume the publisher role. + +Resources: + ArtifactBucket: + Type: AWS::S3::Bucket + # 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 + 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}/*" + - 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:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringEquals: + token.actions.githubusercontent.com:aud: sts.amazonaws.com + StringLike: + token.actions.githubusercontent.com:sub: !Ref TrustedSubject + Policies: + - PolicyName: put-artifact-objects + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: s3:PutObject + Resource: !Sub "${ArtifactBucket.Arn}/*" + +Outputs: + BucketName: + Value: !Ref ArtifactBucket + BucketUrl: + Value: !Sub "https://${ArtifactBucket}.s3.${AWS::Region}.amazonaws.com" + PublisherRoleArn: + Value: !GetAtt PublisherRole.Arn From 368bcf24b42d0a47862f577f64a914c07daa61c3 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Mon, 21 Sep 2026 16:09:54 +0000 Subject: [PATCH 2/4] ci(repo): harden the cli-artifacts stack deploy Use the partition pseudo-parameter in the publisher role trust principal, delete a stack left in ROLLBACK_COMPLETE by a failed initial create before deploying (that state cannot be updated and holds no resources), and print the failed stack events when a deploy fails. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PeXSyQtKtGuwrdNW7uVKom --- .github/workflows/cli-artifacts-infra.yml | 21 +++++++++++++++++++++ infra/cli-artifacts/template.yaml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cli-artifacts-infra.yml b/.github/workflows/cli-artifacts-infra.yml index 49f02188c3..caa2e2629c 100644 --- a/.github/workflows/cli-artifacts-infra.yml +++ b/.github/workflows/cli-artifacts-infra.yml @@ -63,6 +63,19 @@ jobs: 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 @@ -72,6 +85,14 @@ jobs: --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset + - name: Show stack events on failure + if: failure() + run: | + 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: | aws cloudformation describe-stacks \ diff --git a/infra/cli-artifacts/template.yaml b/infra/cli-artifacts/template.yaml index 4368af2522..cd370e2434 100644 --- a/infra/cli-artifacts/template.yaml +++ b/infra/cli-artifacts/template.yaml @@ -78,7 +78,7 @@ Resources: Statement: - Effect: Allow Principal: - Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com + Federated: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com Action: sts:AssumeRoleWithWebIdentity Condition: StringEquals: From 1b7f2564f3bb6f34df8e1d244b968f9f0a2b2c04 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Mon, 21 Sep 2026 16:12:19 +0000 Subject: [PATCH 3/4] ci(repo): tighten the cli-artifacts stack after review Match the publisher trust subject exactly and constrain both template parameters, retain the bucket on stack delete or replace, let the publisher abort its own failed multipart uploads, pass the parameters explicitly on every deploy so a change actually reaches the live stack, scope the workflow concurrency per pull request with a separate serial group for deploys, and put the deploy workflow under CLI maintainer ownership. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PeXSyQtKtGuwrdNW7uVKom --- .github/CODEOWNERS | 4 ++++ .github/workflows/cli-artifacts-infra.yml | 21 ++++++++++++++++++--- infra/cli-artifacts/README.md | 12 +++++++----- infra/cli-artifacts/template.yaml | 18 +++++++++++++----- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b9311829f..8b94b670bc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.github/workflows/cli-artifacts-infra.yml b/.github/workflows/cli-artifacts-infra.yml index caa2e2629c..bf489fb0cd 100644 --- a/.github/workflows/cli-artifacts-infra.yml +++ b/.github/workflows/cli-artifacts-infra.yml @@ -20,9 +20,10 @@ on: permissions: {} +# Lint runs are per pull request or ref; deploys serialize on their own group below. concurrency: - group: cli-artifacts-infra - cancel-in-progress: false + group: cli-artifacts-infra-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: lint: @@ -49,9 +50,18 @@ jobs: 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: @@ -83,11 +93,15 @@ jobs: --stack-name cli-artifacts \ --template-file infra/cli-artifacts/template.yaml \ --capabilities CAPABILITY_NAMED_IAM \ - --no-fail-on-empty-changeset + --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]" \ @@ -95,6 +109,7 @@ jobs: - name: Show stack outputs run: | + set -euo pipefail aws cloudformation describe-stacks \ --stack-name cli-artifacts \ --query "Stacks[0].Outputs" \ diff --git a/infra/cli-artifacts/README.md b/infra/cli-artifacts/README.md index 3905192f1f..9f1a2a76fc 100644 --- a/infra/cli-artifacts/README.md +++ b/infra/cli-artifacts/README.md @@ -11,8 +11,8 @@ agent sandboxes that allow `*.amazonaws.com`. 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 and can do nothing but `s3:PutObject` on the - bucket's objects. The slim native mirror job in `.github/workflows/mirror-slim-image.yml` + `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 @@ -28,8 +28,9 @@ without rewriting. ## How it deploys -`.github/workflows/cli-artifacts-infra.yml` runs `cfn-lint` on every pull request and deploys the -stack on pushes to `develop` with the account's `github-deploy` role. Every other ref can only +`.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. @@ -39,7 +40,8 @@ the per-release upload runs as the bucket-scoped publisher role. ## Changing the trusted branch -Redeploy with a different `TrustedSubject` parameter (for example a release branch). The +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:/:ref:refs/heads/` form that `supabase/cli` currently emits; repositories created or renamed after 2026-07-15 emit immutable `owner@/repo@` subjects instead. diff --git a/infra/cli-artifacts/template.yaml b/infra/cli-artifacts/template.yaml index cd370e2434..cc07288c1e 100644 --- a/infra/cli-artifacts/template.yaml +++ b/infra/cli-artifacts/template.yaml @@ -7,16 +7,21 @@ Parameters: BucketName: Type: String Default: supabase-cli-artifacts - Description: Globally unique bucket name; also the host label in the public URL. - AllowedPattern: "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$" + 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: GitHub Actions OIDC subject allowed to assume the publisher role. + 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: @@ -83,15 +88,18 @@ Resources: Condition: StringEquals: token.actions.githubusercontent.com:aud: sts.amazonaws.com - StringLike: token.actions.githubusercontent.com:sub: !Ref TrustedSubject 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 + Action: + - s3:PutObject + - s3:AbortMultipartUpload Resource: !Sub "${ArtifactBucket.Arn}/*" Outputs: From 7071a19ac26b2ca0b1790dad816af67ec1ca1c53 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Mon, 21 Sep 2026 16:16:17 +0000 Subject: [PATCH 4/4] docs(repo): state the branch-scoped trust of the publisher role Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PeXSyQtKtGuwrdNW7uVKom --- infra/cli-artifacts/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infra/cli-artifacts/README.md b/infra/cli-artifacts/README.md index 9f1a2a76fc..817abdb9d8 100644 --- a/infra/cli-artifacts/README.md +++ b/infra/cli-artifacts/README.md @@ -38,6 +38,13 @@ 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)