Skip to content

Add an S3 publish action - #2

Merged
t4k merged 1 commit into
mainfrom
ci/publish-to-s3
Sep 2, 2026
Merged

Add an S3 publish action#2
t4k merged 1 commit into
mainfrom
ci/publish-to-s3

Conversation

@t4k

@t4k t4k commented Sep 2, 2026

Copy link
Copy Markdown
Member

Uploads built assets to S3 and invalidates the matching CloudFront paths, authenticating with OIDC so no long-lived AWS keys exist anywhere.

Replaces the per-repository publish_to_s3.bash and invalidate_cdn.bash scripts, which needed an uncommitted media.env to exist on someone's machine.

What it does differently from the scripts it replaces

  • The invalidation is scoped to the project's prefix. The old script invalidated /*, which charged for and discarded every other project's cached objects in the same distribution.
  • It refuses to publish an empty or missing source directory. A build that "succeeds" into nothing would otherwise leave the CDN serving stale objects with no signal.
  • Content types are set explicitly per extension. The AWS CLI's guess omits the charset and has changed between versions; publish_to_s3.bash set text/javascript and text/css with charset=utf-8, and the live objects carry those. Replacing it should not quietly change them.
  • The job summary links every published file. S3 website endpoints serve no directory listing — a URL ending in / returns 404 rather than an index — so there is no parent URL to link. Set public-base-url to turn the file list into links.

Things worth knowing before merging

  • It uses cp, not sync --delete. The CL-web-components prefix holds a fonts/ directory that exists in no repository — three woff files that page.tmpl and the footer component load. A delete-enabled sync would remove them and nothing would put them back.
  • --acl public-read is kept, and the reason is in the script: the media bucket is in legacy ObjectWriter mode with no bucket policy, so public read comes entirely from per-object ACLs. Removing the need for it means adding a bucket policy first, verifying the CDN still serves, then setting BucketOwnerEnforced — in that order, or every project on the distribution goes down.
  • The calling job must declare permissions: id-token: write. A composite action cannot.
  • The role's trust policy must name the calling repository's subject claim, which is repo-specific. A role that works for one repository will not work for another until an entry is added.

Testing

CI covers the argument handling and both guards. It holds no AWS credentials, and should not — the end-to-end path was exercised from a fork against separate test resources, with dry_run reporting the same destinations the old script wrote to:

==> dist -> s3://***/cl-webcomponents/
==> css  -> s3://***/cl-webcomponents/css/
==> would invalidate /cl-webcomponents/* on ***

Those paths match publish_to_s3.bash exactly: root bundles to the prefix root, CSS under css/.

Once this merges and v1 moves, I'll re-run that dry run against the released action before the CL-web-components caller is opened.

🤖 Generated with Claude Code

Uploads built assets to an S3 bucket and invalidates the matching CloudFront
paths, authenticating with OIDC so no long-lived AWS keys exist anywhere.
Replaces the per-repository publish_to_s3.bash and invalidate_cdn.bash
scripts, which needed an uncommitted media.env to exist on someone's machine.

The invalidation stays in this action rather than becoming a separate one.
It is cleanup after an upload, not a step anyone would run on its own.

Two guards, both covering ways the old scripts could do damage quietly:

- It refuses to publish an empty or missing source directory. A build that
  "succeeds" into nothing would otherwise leave the CDN serving stale objects
  with no signal that anything went wrong.
- The invalidation is scoped to the project's own prefix. The previous script
  invalidated /*, which charged for and discarded every other project's cached
  objects in the same distribution.

Content types are set explicitly per extension rather than left to the AWS
CLI's guess, which omits the charset and has changed between CLI versions.
publish_to_s3.bash set text/javascript and text/css with charset=utf-8, and
the live objects carry those values; replacing it should not quietly change
them.

It uses `cp`, not `sync --delete`. The CL-web-components prefix holds a fonts/
directory that exists in no repository -- three woff files that page.tmpl and
the footer component load. A delete-enabled sync would remove them and nothing
would put them back.

--acl public-read is kept and the reason recorded: the media bucket is in
legacy ObjectWriter mode with no bucket policy, so public read comes entirely
from per-object ACLs. Removing the need for it means adding a bucket policy
first, verifying the CDN still serves, then setting BucketOwnerEnforced -- in
that order, or every project on the distribution goes down.

The calling job must declare `permissions: id-token: write`; a composite
action cannot. The role's trust policy must name the calling repository's
subject claim, which is repo-specific. Both are documented, because both cost
time to diagnose from the error message alone.

CI tests the argument handling and the guards but holds no AWS credentials.
This repository should not have any.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@t4k
t4k merged commit 6cf27e6 into main Sep 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant