Troubleshooting and the production workflow
The failures that actually happen in CI: lock file errors, OIDC AssumeRoleWithWebIdentity errors, S3 access denied and region mismatches, plus how to add cache invalidation.
Troubleshooting: lock file errors
One of the first errors I encountered was:
Error: Dependencies lock file is not found
The relevant configuration was:
cache: npm
The solution was to make sure the repository contained:
package-lock.json
Then:
npm ci
worked.
If you encounter this problem:
npm install
git add package-lock.json
git commit -m "Add package lock"
git push
Then rerun the workflow.
Troubleshooting: OIDC AssumeRoleWithWebIdentity errors
Another important failure looked like:
Could not assume role with OIDC:
Not authorized to perform sts:AssumeRoleWithWebIdentity
This is different from an S3 permissions error.
It means GitHub successfully reached AWS, but AWS rejected the attempt to assume the IAM role.
The first things to check are:
The IAM permissions policy is not the first thing to troubleshoot.
The trust relationship controls:
Who can assume this role?
The permissions policy controls:
What can the role do after it has been assumed?
Troubleshooting: S3 AccessDenied
If the deployment itself fails with S3 access errors, check:
IAM role permissions
S3 bucket policy
S3 bucket name
AWS region
CloudFront origin configuration
But if the homepage loads while internal links return:
AccessDenied
then don’t immediately assume the IAM policy is wrong.
It may instead be a URL-routing problem.
In this setup, the solution was a CloudFront Function that converts:
/projects
to:
/projects/index.html
Troubleshooting: AWS region
The AWS region must be consistent.
The GitHub Action uses:
aws-region: eu-north-1
The S3 bucket exists in a specific region.
CloudFront then references the S3 origin.
If the region is incorrect, deployment or origin access can fail.
So check the S3 bucket’s actual region, and make sure the aws-region value in the workflow matches
it. A mismatch there is the most common cause of a successful AssumeRole step followed by an S3
access-denied error.
Optional improvement: automatic CloudFront invalidation
CloudFront caches content.
After uploading new files to S3, cached objects can remain available at CloudFront until they expire or are invalidated.
A deployment can therefore optionally perform a cache invalidation after the S3 sync.
The command is:
aws cloudfront create-invalidation \
--distribution-id "$DISTRIBUTION_ID" \
--paths "/*"
The workflow can therefore become:
- name: Invalidate CloudFront
env:
DISTRIBUTION_ID: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
run: |
aws cloudfront create-invalidation \
--distribution-id "$DISTRIBUTION_ID" \
--paths "/*"
This deployment does not use that step today: the sync replaces the files, and a page already cached at the edge keeps serving the previous build until its TTL expires. Add the step when a stale page is worse than an extra API call.
If this is used, the IAM role needs permission for CloudFront invalidation.
For example:
{
"Effect": "Allow",
"Action": [
"cloudfront:CreateInvalidation"
],
"Resource": "arn:aws:cloudfront::YOUR_AWS_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
Astro’s AWS deployment documentation also demonstrates S3 synchronization followed by CloudFront invalidation. Astro AWS deployment documentation
Final production workflow
The workflow that runs on every push is:
name: Deploy Astro to S3
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Build Astro
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsDocumentationBlog
aws-region: eu-north-1
mask-aws-account-id: true
- name: Deploy to S3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
run: |
aws s3 sync ./dist "s3://${AWS_S3_BUCKET}" --delete --only-show-errors
This workflow contains no:
AWS access key
AWS secret access key
personal domain
The account number and the bucket name come from repository secrets, and the region is written directly into the workflow. See Configuring the workflow for why they are secrets and not variables.
The final architecture
The completed system can be summarized as:
The developer experience
The infrastructure may look complicated when written out.
But from the developer’s perspective, it is extremely simple.
I write an article.
src/content/articles/new-article.md
Then:
git add .
git commit -m "Add new article"
git push origin main
And the deployment process takes over.
No FTP.
No manual upload.
No SSH.
No manually running the build on a server.
No permanent AWS credentials stored in GitHub.
Lessons learned
The most useful part of this project was not simply getting an Astro site running.
It was understanding how the individual components fit together.
GitHub
GitHub is the source of truth for the project.
GitHub Actions
GitHub Actions automates the build and deployment.
Astro
Astro converts the source project into static web files.
OIDC
OIDC establishes an identity relationship between GitHub and AWS without requiring long-lived AWS access keys.
IAM
IAM controls both:
Who can assume the deployment role?
and:
What can that role do?
S3
S3 stores the generated website.
CloudFront
CloudFront provides the public distribution layer, caching and HTTPS.
CloudFront Functions
CloudFront Functions can modify incoming URLs before they reach S3, which solves the clean URL problem for Astro’s generated index.html pages.
The most important security principle
The final architecture follows a simple principle:
Give the deployment system exactly the permissions it needs, and nothing more.
GitHub Actions does not receive administrator access to AWS.
It receives a specific role.
That role is restricted by:
Repository
+
Branch
+
OIDC identity
+
Specific AWS permissions
+
Specific S3 bucket
This is much better than giving a CI system broad AWS permissions.
Final result
What started as:
"I want an engineering blog."
became a complete CI/CD system:
And publishing an article is now simply:
git push
That is the part I like most about the setup.
The infrastructure is doing its job in the background, while the development workflow stays simple.