Skip to content

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-central1 or europe-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):

  1. Delete Expired Archives:
    • Action: Delete object.
    • Condition: Age is 30 days (or 60 days).
  2. 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

  1. In the Google Cloud Console, navigate to IAM & Admin > Service Accounts > Create Service Account.
  2. Name: github-actions-ci-cache.
  3. 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:

  1. Navigate to Cloud Storage > Buckets > Click your cache bucket.
  2. Under the Permissions tab, click Grant Access.
  3. New principals: Enter the email of your dedicated service account (github-actions-ci-cache@<project-id>.iam.gserviceaccount.com).
  4. 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

  1. In the Google Cloud Console, navigate to Cloud Storage > Settings > Interoperability.
  2. Under Interoperability keys for service accounts, click Create a key for a service account.
  3. Select your github-actions-ci-cache service account.
  4. 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 NameDescription
GCS_HMAC_ACCESS_IDThe generated HMAC Access ID (starts with GOOG...)
GCS_HMAC_SECRETThe generated HMAC Secret key

Workflow Configuration

yaml
- 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/caches

NOTE

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)
yaml
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"