AWS S3 Setup
Cloud Cache Action supports AWS S3 out of the box using both GitHub Actions OIDC (OpenID Connect) with IAM Roles and static IAM credentials.
Best Practices: Bucket Setup & Configuration
Follow these recommendations when creating and configuring your AWS S3 cache bucket:
1. Bucket Region & Colocation
- GitHub-hosted runners: Default
ubuntu-latestandwindows-latestrunners typically execute in AWS US regions (us-east-1orus-east-2). Creating your bucket inus-east-1minimizes cross-region latency and lowers data transfer fees. - Self-hosted EC2 runners: Always create the bucket in the same AWS region and VPC as your runners to achieve maximum throughput (line-rate VPC speeds) with zero data transfer costs.
2. Security & Access Control
- Block All Public Access: Ensure all 4 settings under Block Public Access are enabled. Cache bundles contain compiled binaries, source artifacts, and dependency manifests that must never be publicly readable.
- Object Ownership: Enable Bucket owner enforced (disable ACLs) to guarantee consistent ownership of all uploaded archives.
- Default Encryption: Use server-side encryption with Amazon S3 managed keys (SSE-S3 /
AES256) or AWS KMS (SSE-KMS). SSE-S3 is included at no additional cost.
3. Lifecycle Rules & Cost Optimization
Without lifecycle management, older cache revisions accumulate and increase storage costs. Configure two lifecycle rules under Bucket Management > Lifecycle Rules:
- Expire Current Objects:
- Filter: Apply to all objects in bucket (or prefix
${GITHUB_REPOSITORY}/). - Action: Expire current versions of objects after 30 or 60 days.
- Filter: Apply to all objects in bucket (or prefix
- Abort Incomplete Multipart Uploads:
- Action: Delete expired object delete markers and incomplete multipart uploads after 7 days. This prevents lingering chunks from failed or interrupted uploads from consuming storage.
Credentials & Least-Privilege IAM Policies
Option A: GitHub Actions OIDC (Recommended)
Using GitHub Actions OpenID Connect (OIDC) eliminates the need to store long-lived AWS Access Keys in repository secrets.
1. Configure the IAM Role Trust Policy
Create an IAM Role with a trust policy allowing GitHub Actions to assume it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<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:<OWNER>/<REPO>:*"
}
}
}
]
}2. Attach Least-Privilege S3 Permissions Policy
Attach an IAM policy granting only the minimal actions required by cloud-cache-action:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowBucketListing",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-actions-cache-bucket"
},
{
"Sid": "AllowObjectOperations",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:AbortMultipartUpload"
],
"Resource": "arn:aws:s3:::my-actions-cache-bucket/*"
}
]
}NOTE
cloud-cache-action does not require s3:DeleteObject. Lifecycle cleanup is handled by bucket lifecycle rules.
3. Workflow Example (OIDC)
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for requesting the OIDC JWT
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsCacheRole
aws-region: us-east-1
- name: Cache dependencies
uses: xSAVIKx/cloud-cache-action@v1
with:
bucket: my-actions-cache-bucket
key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
path: node_modulesOption B: Static IAM Credentials
If you prefer static credentials, create a dedicated IAM user (never use your root AWS account) with the least-privilege policy shown above, generate an Access Key ID and Secret Access Key, and save them in your repository's GitHub Secrets.
- name: Cache dependencies
uses: xSAVIKx/cloud-cache-action@v1
with:
bucket: my-actions-cache-bucket
region: us-east-1
access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
path: node_modulesLive CI Verification Workflow
This action is tested continuously against a real Amazon S3 bucket. You can inspect the live GitHub Actions workflow file in the repository: .github/workflows/provider-aws-s3.yml.
.github/workflows/provider-aws-s3.yml (Click to view full workflow)
name: Provider - Amazon S3
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: '0 4 * * *'
permissions:
id-token: write
contents: read
jobs:
test-aws-s3:
name: Amazon S3 Live Integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Check AWS credentials
id: check-secrets
run: |
BUCKET="${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}"
ROLE="${{ secrets.AWS_ROLE_TO_ASSUME }}"
ACCESS_KEY="${{ secrets.AWS_ACCESS_KEY_ID }}"
SECRET_KEY="${{ secrets.AWS_SECRET_ACCESS_KEY }}"
if [ -z "$BUCKET" ]; then
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "::notice::AWS S3 bucket secret (AWS_S3_BUCKET or AWS_BUCKET) is not configured. Skipping live AWS S3 integration test."
elif [ -n "$ROLE" ]; then
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "auth_mode=oidc" >> $GITHUB_OUTPUT
elif [ -n "$ACCESS_KEY" ] && [ -n "$SECRET_KEY" ]; then
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "auth_mode=static" >> $GITHUB_OUTPUT
else
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "::notice::AWS credentials (AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY, or AWS_ROLE_TO_ASSUME) are not configured. Skipping live AWS S3 integration test."
fi
- name: Configure AWS Credentials via OIDC
if: steps.check-secrets.outputs.has_secrets == 'true' && steps.check-secrets.outputs.auth_mode == 'oidc'
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ secrets.AWS_REGION || 'us-east-1' }}
- name: Generate synthetic test payload
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
mkdir -p /tmp/aws-cache-test
echo "AWS S3 test artifact $(date +%s%N) for run ${{ github.run_id }}" > /tmp/aws-cache-test/sample.txt
sha256sum /tmp/aws-cache-test/sample.txt > /tmp/aws-expected-sha.txt
cat /tmp/aws-expected-sha.txt
- name: Save cache to Amazon S3 (save-only)
if: steps.check-secrets.outputs.has_secrets == 'true'
uses: ./save
with:
path: /tmp/aws-cache-test
key: aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}
bucket: ${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}
region: ${{ secrets.AWS_REGION || 'us-east-1' }}
access-key: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}
- name: Purge local test payload
if: steps.check-secrets.outputs.has_secrets == 'true'
run: rm -rf /tmp/aws-cache-test
- name: Restore cache from Amazon S3 (restore-only)
if: steps.check-secrets.outputs.has_secrets == 'true'
id: restore-aws
uses: ./restore
with:
path: /tmp/aws-cache-test
key: aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}
bucket: ${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}
region: ${{ secrets.AWS_REGION || 'us-east-1' }}
access-key: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}
fail-on-cache-miss: true
- name: Verify restored content and checksum
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
echo "Asserting cache hit..."
if [ "${{ steps.restore-aws.outputs.cache-hit }}" != "true" ]; then
echo "::error::Expected cache-hit to be 'true', got '${{ steps.restore-aws.outputs.cache-hit }}'"
exit 1
fi
if [ ! -f /tmp/aws-cache-test/sample.txt ]; then
echo "::error::Restored file /tmp/aws-cache-test/sample.txt not found!"
exit 1
fi
ACTUAL_SHA=$(sha256sum /tmp/aws-cache-test/sample.txt | awk '{print $1}')
EXPECTED_SHA=$(awk '{print $1}' /tmp/aws-expected-sha.txt)
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "::error::Checksum mismatch! Expected $EXPECTED_SHA, got $ACTUAL_SHA"
exit 1
fi
echo "Live Amazon S3 cache save & restore test succeeded! Checksum: $ACTUAL_SHA"