Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The plugin supports various backends and compression algorithms, and some enviro
- `tar` for tar compression
- `zip/unzip` for zip compression
- `aws` AWS CLI for reading and writing to an AWS S3 backend
- `gsutil` or `gcloud storage` Google Cloud SDK CLI for reading and writing to a GCS backend

## Mandatory parameters

Expand All @@ -62,6 +63,7 @@ You can specify multiple levels in an array to save the same artifact as a cache
Defines how the cache is stored and restored. Can be any string (see [Customizable Backends](#customizable-backends)), but the plugin natively supports the following:
* `fs` (default)
* `s3`
* `gcs`

#### `fs`

Expand Down Expand Up @@ -130,6 +132,38 @@ steps:
compression: zstd
```

#### `gcs`

Store things in a Google Cloud Storage (GCS) bucket. The backend automatically detects and uses either `gcloud storage` (preferred) or `gsutil` CLI tools, whichever is available. You need to make sure at least one of these commands is available and appropriately configured with the necessary credentials and access permissions.

You also need the agent to have access to the following defined environment variables:
* `BUILDKITE_PLUGIN_GCS_CACHE_BUCKET`: the bucket to use (backend will fail if not defined)
* `BUILDKITE_PLUGIN_GCS_CACHE_PREFIX`: optional prefix to use for the cache within the bucket
* `BUILDKITE_PLUGIN_GCS_CACHE_CLI`: optional CLI preference, either `gcloud` or `gsutil` (auto-detects if not set, preferring `gcloud storage`)

Setting the `BUILDKITE_PLUGIN_GCS_CACHE_QUIET` environment variable will reduce logging of file operations to GCS.

#### Example

```yaml
env:
BUILDKITE_PLUGIN_GCS_CACHE_BUCKET: "my-cache-bucket" # Required: GCS bucket to store cache objects
BUILDKITE_PLUGIN_GCS_CACHE_PREFIX: "buildkite/cache"
BUILDKITE_PLUGIN_GCS_CACHE_QUIET: "true"

steps:
- label: ':nodejs: Install dependencies'
command: npm ci
plugins:
- cache#v1.7.0:
backend: gcs
path: node_modules
manifest: package-lock.json
restore: file
save: file
compression: zstd
```

### `compression` (string)

Allows for the cached file/folder to be saved/restored as a single file. You will need to make sure to use the same compression when saving and restoring or it will cause a cache miss.
Expand Down
142 changes: 142 additions & 0 deletions backends/cache_gcs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/bin/bash

if [ -z "${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}" ]; then
echo '+++ 🚨 Missing GCS bucket configuration'
exit 1
fi

# Detect which GCS CLI to use
detect_gcs_cli() {
# Check for explicit preference
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_CLI}" ]; then
echo "${BUILDKITE_PLUGIN_GCS_CACHE_CLI}"
return
fi

# Auto-detect: prefer gcloud storage if available
if command -v gcloud &>/dev/null && gcloud storage --help &>/dev/null 2>&1; then
echo "gcloud"
elif command -v gsutil &>/dev/null; then
echo "gsutil"
else
echo '+++ 🚨 Neither gcloud storage nor gsutil found' >&2
exit 1
fi
}

# Lazy load CLI selection
get_gcs_cli() {
if [ -z "${GCS_CLI}" ]; then
GCS_CLI=$(detect_gcs_cli)
fi
echo "${GCS_CLI}"
}

build_key() {
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_PREFIX}" ]; then
echo "${BUILDKITE_PLUGIN_GCS_CACHE_PREFIX}/${1}"
else
echo "$1"
fi
}

gcs_cmd() {
local cmd_args=()
local cli
cli=$(get_gcs_cli)

if [ "${cli}" = "gcloud" ]; then
cmd_args=(gcloud storage)
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_QUIET}" ]; then
cmd_args+=(--verbosity=none)
fi
else
cmd_args=(gsutil)
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_QUIET}" ]; then
cmd_args+=(-q)
fi
fi

"${cmd_args[@]}" "$@"
}

gcs_copy() {
local from="$1"
local to="$2"
local use_rsync="${3:-true}"
local cli
cli=$(get_gcs_cli)

if [ "${use_rsync}" = 'true' ]; then
# Use rsync for directories
if [ "${cli}" = "gcloud" ]; then
gcs_cmd rsync -r -d "${from}" "${to}"
else
gcs_cmd -m rsync -r -d "${from}" "${to}"
fi
else
# Use cp for single files
gcs_cmd cp "${from}" "${to}"
fi
}

gcs_exists() {
local key="$1"
local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/$(build_key "${key}")"
Copy link
Contributor

@JoeColeman95 JoeColeman95 Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a spellchecker issue

Suggested change
local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/$(build_key "${key}")"
local full_path
full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/$(build_key "${key}")"


# Check if the object exists using ls
# For directories, ls will list contents
# For files, it will return the file path
if gcs_cmd ls "${full_path}*" &>/dev/null; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for us to do this without the wildcard? As this may cause some false positives

return 0
else
return 1
fi
}

restore_cache() {
local from="$1"
local to="$2"
local use_rsync='false'
local key="$(build_key "${from}")"
Copy link
Contributor

@JoeColeman95 JoeColeman95 Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a spellchecker issue

Suggested change
local key="$(build_key "${from}")"
local key
key="$(build_key "${from}")"

local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/${key}"

# Check if it's a directory by trying to list it as a prefix
if gcs_cmd ls "${full_path}/" &>/dev/null; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be preferable here to use ls -d here if it's gsutil or the --max-results=1 flag here if it's gcloud as this could be a little fragile

use_rsync='true'
fi

gcs_copy "${full_path}" "${to}" "${use_rsync}"
}

save_cache() {
local to="$1"
local from="$2"
local use_rsync='true'
local key="$(build_key "${to}")"
Copy link
Contributor

@JoeColeman95 JoeColeman95 Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a spellchecker issue

Suggested change
local key="$(build_key "${to}")"
local key
key="$(build_key "${to}")"

local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/${key}"

if [ -f "${from}" ]; then
use_rsync='false'
fi

gcs_copy "${from}" "${full_path}" "${use_rsync}"
}

exists_cache() {
if [ -z "$1" ]; then exit 1; fi
gcs_exists "$1"
}

OPCODE="$1"
shift

if [ "$OPCODE" = 'exists' ]; then
exists_cache "$@"
elif [ "$OPCODE" = 'get' ]; then
restore_cache "$@"
elif [ "$OPCODE" = 'save' ]; then
save_cache "$@"
else
exit 255
fi
Loading