This guide walks through deploying Scryglass to Amazon S3 + CloudFront using
the GitHub Actions workflow in .github/workflows/deploy-aws.yml.
By the end of this guide you will have a production deployment that:
- Builds both
@scryglass/coreand@scryglass/pwain CI - Uploads the Vite build output to an S3 bucket
- Serves the site through CloudFront with proper cache headers
- Deploys automatically on every push to
main
- An AWS account with permissions to create the resources below
- A GitHub repository (fork or clone of Scryglass)
- The AWS CLI installed locally (helpful for verification, not strictly required)
- Open the S3 console and create a new
bucket (e.g.
scryglass-prod). - Block all public access — CloudFront will serve the files; the bucket does not need to be public.
- Enable static website hosting under the bucket properties (set the index
document to
index.html). This is optional when using CloudFront with an Origin Access Control, but useful for debugging.
- Open the CloudFront console and create a new distribution.
- Set the origin to the S3 bucket created above. Use an Origin Access Control (OAC) so CloudFront can read from the private bucket.
- Set the default root object to
index.html. - Under Error pages, add a custom error response for HTTP 403 and 404 that
returns
/index.htmlwith status 200. This enables client-side routing. - Note the Distribution ID and domain name (e.g.
d1234567890abc.cloudfront.net) — you will need both later.
This lets GitHub Actions assume an IAM role without long-lived credentials.
- Open the IAM console → Identity providers.
- Click Add provider → OpenID Connect.
- Set:
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
- Provider URL:
- Click Add provider.
-
In the IAM console, create a new role.
-
Choose Web identity as the trusted entity, select the GitHub OIDC provider created above, and set the audience to
sts.amazonaws.com. -
Add a trust policy condition that restricts the role to your repository and branch. Replace
YOUR_ORG/YOUR_REPOwith your GitHub owner/repo:{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID: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": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main" } } } ] } -
Attach an inline policy (or managed policy) granting the minimum permissions needed to deploy. Replace
YOUR_BUCKET_NAMEandYOUR_DISTRIBUTION_ID:{ "Version": "2012-10-17", "Statement": [ { "Sid": "S3Deploy", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::YOUR_BUCKET_NAME", "arn:aws:s3:::YOUR_BUCKET_NAME/*" ] }, { "Sid": "CloudFrontInvalidate", "Effect": "Allow", "Action": "cloudfront:CreateInvalidation", "Resource": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID" } ] } -
Note the Role ARN (e.g.
arn:aws:iam::123456789012:role/scryglass-deploy).
In your repository, go to Settings → Secrets and variables → Actions → Variables and add:
| Variable | Description | Example |
|---|---|---|
AWS_ROLE_ARN |
ARN of the IAM deploy role | arn:aws:iam::123456789012:role/scryglass-deploy |
AWS_REGION |
AWS region of the S3 bucket | us-east-1 |
S3_BUCKET_NAME |
Name of the S3 bucket | scryglass-prod |
CLOUDFRONT_DISTRIBUTION_ID |
CloudFront distribution ID | E1234567890ABC |
CLOUDFRONT_DOMAIN (optional) |
CloudFront domain for the smoke test step | d1234567890abc.cloudfront.net |
!!! note These are repository variables (not secrets). They are not sensitive because the OIDC trust policy already restricts who can assume the role.
Deployments happen automatically on push to main. You can also trigger a deploy
manually:
- Go to Actions → Deploy to AWS (S3 + CloudFront).
- Click Run workflow → select the
mainbranch → Run workflow.
The workflow (.github/workflows/deploy-aws.yml) runs these steps:
- Checkout the repository.
- Install dependencies with
npm ci(npm workspaces resolve both packages). - Build with
npm run build(compiles@scryglass/corethen builds@scryglass/pwawith Vite, producingpackages/pwa/dist/). - Configure AWS credentials via GitHub OIDC (no stored secrets needed).
- Sync to S3 with differentiated cache headers:
dist/assets/*(Vite's hashed files) →Cache-Control: public, max-age=31536000, immutable(1 year — safe because filenames change on every build)index.html→Cache-Control: public, max-age=60, s-maxage=300(short browser cache, slightly longer CDN cache)sw.js→Cache-Control: public, max-age=0, must-revalidate(always re-validate so users get service worker updates immediately)
- Invalidate CloudFront so edge caches serve the new content.
- Smoke test (optional) — if
CLOUDFRONT_DOMAINis set, curl the site and verify a 200 response.
- Verify the OIDC provider URL and audience in IAM match exactly.
- Check the trust policy
subcondition matches your repo and branch.
- Ensure the IAM policy includes
s3:ListBucketon the bucket ARN (not justs3:*Objecton the objects). - Confirm the bucket name in the GitHub variable matches the actual bucket.
- The workflow creates an invalidation on every deploy. Propagation takes 1-2 minutes. Wait and retry.
- For debugging, check the invalidation status in the CloudFront console.
- The smoke test only runs when the
CLOUDFRONT_DOMAINvariable is set. Add it to your repository variables if you want the check.