Google Cloud Storage (GCS) Setup
Google Cloud Storage (GCS) provides full S3 interoperability via its XML API and HMAC service account keys, allowing you to use GCS as a high-speed caching tier for your GitHub Actions workflows.
Best Practices: Bucket Setup & Configuration
Follow these recommendations when creating your GCS cache bucket:
1. Bucket Location & Storage Class
- Location Type: Choose a Region bucket (e.g.,
us-central1oreurope-west1) colocated with your runners. Avoid multi-region or dual-region buckets unless required for multi-continent runners, as regional buckets offer lower latency and avoid inter-region network charges. - Storage Class: Always select Standard. Never use Nearline, Coldline, or Archive storage classes for CI caches. These colder tiers charge data retrieval fees every time a cache is restored and enforce minimum storage retention periods (30 to 90 days), resulting in higher costs for frequently updated caches.
2. Access Control & Security
- Public Access Prevention: Enable Enforce public access prevention on the bucket to ensure archives are never exposed publicly.
- Access Control Model: Select Uniform bucket-level access (Google's recommended best practice), which uses Cloud IAM policies exclusively rather than legacy object-level ACLs.
- Data Protection: Keep Soft Delete enabled or configure a 7-day retention period if you want protection against accidental deletion.
3. Object Lifecycle Management
Configure an automated lifecycle policy in the Google Cloud Console (Bucket > Lifecycle > Add a rule):
- Delete Expired Archives:
- Action: Delete object.
- Condition: Age is 30 days (or 60 days).
- Abort Incomplete Multipart Uploads:
- Action: Abort incomplete multipart uploads after 7 days.
Credentials & Interoperability Setup
GCS supports S3 API requests using HMAC (Hash-based Message Authentication Code) credentials.
IMPORTANT
Always create HMAC keys for a dedicated Service Account, never for personal user Google accounts. User HMAC keys are tied to individual accounts and break when employees change roles or leave an organization.
1. Create a Dedicated Service Account
- In the Google Cloud Console, navigate to IAM & Admin > Service Accounts > Create Service Account.
- Name:
github-actions-ci-cache. - Click Create and Continue.
2. Grant Least-Privilege Bucket Permissions
Do not grant project-wide Editor or Storage Admin roles. Grant access strictly on your cache bucket:
- Navigate to Cloud Storage > Buckets > Click your cache bucket.
- Under the Permissions tab, click Grant Access.
- New principals: Enter the email of your dedicated service account (
github-actions-ci-cache@<project-id>.iam.gserviceaccount.com). - Role: Select Storage Object User (
roles/storage.objectUser), which allows reading, writing, and listing objects without bucket administrative privileges.
3. Generate HMAC Key for the Service Account
- In the Google Cloud Console, navigate to Cloud Storage > Settings > Interoperability.
- Under Interoperability keys for service accounts, click Create a key for a service account.
- Select your
github-actions-ci-cacheservice account. - Copy the generated Access ID (acts as the access key) and Secret (acts as the secret key).
4. Configure GitHub Secrets
Store the credentials in your repository's Settings > Secrets and variables > Actions:
| Secret Name | Description |
|---|---|
GCS_HMAC_ACCESS_ID | The generated HMAC Access ID (starts with GOOG...) |
GCS_HMAC_SECRET | The generated HMAC Secret key |
Workflow Configuration
- name: Cache dependencies using Google Cloud Storage
uses: xSAVIKx/cloud-cache-action@v1
with:
bucket: my-gcs-cache-bucket
endpoint: https://storage.googleapis.com
access-key: ${{ secrets.GCS_HMAC_ACCESS_ID }}
secret-key: ${{ secrets.GCS_HMAC_SECRET }}
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
path: ~/.gradle/cachesNOTE
Path-Style Addressing: Google Cloud Storage XML API requires path-style addressing (force-path-style: true). Cloud Cache Action automatically detects storage.googleapis.com and sets path-style addressing and region defaults without extra configuration.
Live CI Verification Workflow
This action is tested continuously against Google Cloud Storage using HMAC keys and the XML API. You can inspect the live GitHub Actions workflow file in the repository: .github/workflows/provider-gcs.yml.
.github/workflows/provider-gcs.yml (Click to view full workflow)
name: Provider - Google Cloud Storage
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: '0 4 * * *'
jobs:
test-gcs:
name: Google Cloud Storage 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 Google Cloud Storage credentials
id: check-secrets
run: |
ACCESS_ID="${{ secrets.GCS_HMAC_ACCESS_ID || secrets.GCS_ACCESS_KEY_ID }}"
SECRET_KEY="${{ secrets.GCS_HMAC_SECRET || secrets.GCS_SECRET_ACCESS_KEY }}"
BUCKET="${{ secrets.GCS_BUCKET }}"
if [ -z "$ACCESS_ID" ] || [ -z "$SECRET_KEY" ] || [ -z "$BUCKET" ]; then
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "::notice::Google Cloud Storage secrets (GCS_BUCKET, GCS_HMAC_ACCESS_ID/GCS_ACCESS_KEY_ID, GCS_HMAC_SECRET/GCS_SECRET_ACCESS_KEY) are not configured. Skipping live GCS integration test."
else
echo "has_secrets=true" >> $GITHUB_OUTPUT
fi
- name: Generate synthetic test payload
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
mkdir -p /tmp/gcs-cache-test
echo "GCS test artifact $(date +%s%N) for run ${{ github.run_id }}" > /tmp/gcs-cache-test/sample.txt
sha256sum /tmp/gcs-cache-test/sample.txt > /tmp/gcs-expected-sha.txt
cat /tmp/gcs-expected-sha.txt
- name: Save cache to Google Cloud Storage (save-only)
if: steps.check-secrets.outputs.has_secrets == 'true'
uses: ./save
with:
path: /tmp/gcs-cache-test
key: gcs-live-test-${{ github.run_id }}-${{ github.run_attempt }}
bucket: ${{ secrets.GCS_BUCKET }}
endpoint: https://storage.googleapis.com
access-key: ${{ secrets.GCS_HMAC_ACCESS_ID || secrets.GCS_ACCESS_KEY_ID }}
secret-key: ${{ secrets.GCS_HMAC_SECRET || secrets.GCS_SECRET_ACCESS_KEY }}
- name: Purge local test payload
if: steps.check-secrets.outputs.has_secrets == 'true'
run: rm -rf /tmp/gcs-cache-test
- name: Restore cache from Google Cloud Storage (restore-only)
if: steps.check-secrets.outputs.has_secrets == 'true'
id: restore-gcs
uses: ./restore
with:
path: /tmp/gcs-cache-test
key: gcs-live-test-${{ github.run_id }}-${{ github.run_attempt }}
bucket: ${{ secrets.GCS_BUCKET }}
endpoint: https://storage.googleapis.com
access-key: ${{ secrets.GCS_HMAC_ACCESS_ID || secrets.GCS_ACCESS_KEY_ID }}
secret-key: ${{ secrets.GCS_HMAC_SECRET || secrets.GCS_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-gcs.outputs.cache-hit }}" != "true" ]; then
echo "::error::Expected cache-hit to be 'true', got '${{ steps.restore-gcs.outputs.cache-hit }}'"
exit 1
fi
if [ ! -f /tmp/gcs-cache-test/sample.txt ]; then
echo "::error::Restored file /tmp/gcs-cache-test/sample.txt not found!"
exit 1
fi
ACTUAL_SHA=$(sha256sum /tmp/gcs-cache-test/sample.txt | awk '{print $1}')
EXPECTED_SHA=$(awk '{print $1}' /tmp/gcs-expected-sha.txt)
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "::error::Checksum mismatch! Expected $EXPECTED_SHA, got $ACTUAL_SHA"
exit 1
fi
echo "Live Google Cloud Storage cache save & restore test succeeded! Checksum: $ACTUAL_SHA"