Skip to content

Getting Started ​

Cloud Cache Action allows you to store and retrieve GitHub Actions cache bundles using any S3-compatible cloud or self-hosted object storage service.

It is designed as a drop-in replacement for actions/cache@v4-v6, keeping your CI/CD workflows lightning fast while bypassing GitHub's 10GB default cache quotas and egress fees.

Quickstart ​

Add the following step to your GitHub Actions workflow:

yaml
- name: Cache dependencies to S3
  uses: xSAVIKx/cloud-cache-action@v1
  with:
    bucket: my-ci-cache-bucket
    endpoint: https://<account_id>.r2.cloudflarestorage.com # Or AWS, GCS, B2, MinIO, etc.
    access-key: ${{ secrets.S3_ACCESS_KEY }}
    secret-key: ${{ secrets.S3_SECRET_KEY }}
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

For a package manager cache, preset: npm (or yarn, pnpm, bun, pip, poetry, uv, gradle, maven, cargo, go) gives the path, key and restore-keys for you. See Cache Presets.

Paths and Exclusions ​

path accepts files, directories, globs, ~ and ! exclusions, one per line.

  • Exclusions only remove what the include patterns matched, as in actions/cache. path: logs with !logs/debug.txt still caches the whole logs directory, because the directory itself is the match. To leave one file out, match the files instead:

    yaml
    path: |
      logs/*
      !logs/debug.txt
  • Symbolic links are not followed while matching. A pattern that wildcards through a symlinked directory, such as linked-dir/* where linked-dir is a symlink, matches nothing. A symlink that a pattern matches is archived as a link, not as the files it points to.

Symlinks are archived as links, never followed and re-created as copies. On Windows, windows-latest runners use Git's bundled GNU tar, which this action runs with MSYS=winsymlinks:nativestrict (the same setting actions/cache uses) so that restored links come back as native NTFS symlinks rather than plain-text stand-ins. GitHub-hosted Windows runners allow creating these without extra privileges.

On a self-hosted Windows runner, the account can lack the SeCreateSymbolicLinkPrivilege privilege, and Developer Mode can be off. Then Git's GNU tar cannot create native symlinks, and the extract fails with Cannot create symlink to ...: Operation not permitted. When symlinks are the only failure, the action logs a warning and extracts the archive one more time with MSYS=winsymlinks:deepcopy. Each symlink is then restored as a copy of its target, not as a link. A symlink whose target is not in the archive still fails the restore. BSD tar (C:\Windows\System32\tar.exe) does not read MSYS, so the action does not retry with it. This retry applies only to the S3 tier in file mode. With streaming: true, or for the GitHub tier with dual-cache: true (@actions/cache runs its own tar), the restore still fails. The retry also fails for a symlink whose target is another symlink later in the archive. A copied directory symlink can make the restored tree much larger than the archive.

How It Works ​

  1. Restore Phase (Pre/Main):

    • Connects to your S3 bucket using modern @aws-sdk/client-s3.
    • Checks if an exact match exists for the key parameter.
    • If not found, evaluates restore-keys in order and downloads the most recently updated matching archive.
    • Downloads an archive larger than download-chunk-size (default 8 MiB) as concurrent Range requests, download-concurrency (default 8) at a time, each retried on its own; set download-concurrency: 1 for a single request. A provider that ignores Range gets the single request automatically. With transfer-tuning: auto, the action selects these settings from the archive size (see Automatic tuning).
    • Verifies the archive's sha256 checksum, when the object carries one, before extracting it; a mismatch is logged as a warning and counts as a cache miss (with dual-cache: true and dual-cache-strict: true, it fails the step).
    • With streaming: true, extracts the archive as it downloads instead, verifying the checksum at the end: a network or tar failure mid-stream, or a checksum mismatch, becomes a cache miss with the workspace possibly partly extracted, and there is no whole-download retry as there is in the default file mode.
    • Decompresses the archive using zstd (or gzip fallback) directly into your workspace.
    • Sets outputs (cache-hit, cache-primary-key, cache-matched-key, cache-size, cache-storage-provider, cache-s3-key, cache-etag, cache-metadata, cache-hit-source) plus the timings described in Metrics and Timings.
    • Writes a job summary table with the key, hit status, source, size and duration, unless job-summary: false.
  2. Save Phase (Post):

    • If read-only: true or if an exact key match occurred during restore, saving is automatically skipped.
    • Checks whether another job already saved the same object first; if so, keeps that job's cache instead of overwriting it. The upload itself is a conditional create, so two saves racing for the same key cannot overwrite each other on providers that enforce If-None-Match (verified on AWS S3, MinIO and SeaweedFS); providers that ignore it (verified: Garage; reportedly Google Cloud Storage's S3 interoperability) keep last-writer-wins.
    • Otherwise, archives the specified path directories using multi-threaded zstd compression.
    • Streams the compressed archive to your S3 bucket using multipart uploads via @aws-sdk/lib-storage, upload-concurrency (default 8) parts of upload-chunk-size (default 64 MiB) at a time, tagged with a sha256 checksum for later integrity verification.
    • Stores any metadata as x-amz-meta-* user metadata on the saved object and any tags as object tags, and the metadata comes back on the next restore as the cache-metadata output. Providers without object tagging log one warning and save without tags (verified: SeaweedFS and MinIO store tags; Garage accepts the upload but implements no tagging API, so its tags cannot be read back).
    • Emits diagnostics and completes cleanly without breaking the build on non-fatal network interruptions.
    • Writes a job summary table with the key, saved-to tiers, size and duration, unless job-summary: false.

Cache archives are never deleted automatically; see Pruning Caches for a scheduled cleanup sub-action.

Inspecting a Lookup ​

When a restore misses and you expected a hit, set explain: true to log the whole lookup — the resolved key pattern, the ${version} hash and what it is computed from, every ref searched and every candidate object with the version it carries — right before the restore runs. The inspect sub-action prints the same report as a step of its own, restoring nothing and exposing the result as outputs (would-hit, would-match-key, would-match-object, candidate-count, report):

yaml
- name: Inspect the cache lookup
  id: lookup
  uses: xSAVIKx/cloud-cache-action/inspect@v1
  with:
    bucket: my-ci-cache-bucket
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

See Inspecting Lookups for the full report, how to read the "none has version" reason, and the fail-on-cache-miss guard pattern for matrix jobs.

Metrics and Timings ​

Restore and save set four timing and size outputs on every path, so they are always defined: cache-restore-duration-ms and cache-save-duration-ms (wall-clock milliseconds of the step, 0 when it did not complete), cache-transfer-duration-ms (the S3 download or upload alone) and cache-bytes (the archive that was restored or saved, 0 when none was).

On the unified action the save runs as a post step, and it sets cache-save-duration-ms, cache-transfer-duration-ms and cache-bytes for itself — including zeroing them when there is nothing to save. After the post step those three therefore describe the save, while cache-size and cache-restore-duration-ms still hold the restore's values. Read a restore's byte count in a step that runs before the post step, or use the standalone restore and save actions, where each step owns its outputs.

Every step — restore, save, prune and inspect — also writes one cloud-cache-metrics <json> debug line. Set metrics-file to append that same JSON as one line to a file, resolved relative to GITHUB_WORKSPACE:

yaml
- uses: xSAVIKx/cloud-cache-action@v1
  with:
    bucket: my-ci-cache-bucket
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    metrics-file: cache-metrics.jsonl
json
{"step":"restore","timestamp":"2026-09-17T06:02:41.912Z","provider":"r2","key":"Linux-node-9f2c1a","matchedKey":"Linux-node-9f2c1a","objectKey":"octo/app/refs%2Fheads%2Fmain/Linux-node-9f2c1a/4d0f1b2c9a7e35f1/cache.tar.zst","source":"s3","bytes":199687424,"durationMs":8123,"transferDurationMs":5310,"lookupDurationMs":240,"extractDurationMs":2410,"streaming":false,"downloadParts":24,"outcome":"hit"}
  • step is restore, save, prune or inspect, and outcome is one of hit, miss, saved, exists, skipped, error, pruned, would-hit or would-miss.
  • transferDurationMs measures the S3 transfer alone in the default file mode. With streaming: true the archive is extracted (or compressed) as it moves, so the same field covers download-plus-extract; the streaming field on the line tells the two apart.
  • downloadParts on a restore is how many ranged requests fetched the archive: 1 for a single request and 0 when nothing was downloaded.
  • lookupDurationMs on a restore is the time to find the S3 object. In file mode, archiveDurationMs on a save is the time to write and hash the archive, and extractDurationMs on a restore is the time to unpack it. Streaming mode does these phases during the transfer, so it does not report them. A streaming step that falls back to file mode reports them.
  • After an S3 download or upload, the step also logs the phases in one line, for example Timings: lookup 0.24 s, download 5.31 s (35.9 MiB/s), extract 2.41 s, total 8.12 s. This line is not a debug line, so it shows in every log. total is the whole step, including the GitHub tier in dual-cache mode. The transfer time and the rate include retries.
  • prune and inspect write their line once the step has finished its work, so a step that fails earlier writes none. prune reports its tallies in extra (prunedCount, prunedBytes, keptCount, dryRun) and inspect reports candidateCount.
  • Writing the file is best-effort by design: a machine-readable timing record must never fail a cache step, so a write error only logs Could not write metrics to <path>: <reason>.

Standalone Restore and Save Actions ​

Just like actions/cache/restore and actions/cache/save, you can invoke restore and save as independent steps:

Restore Only ​

yaml
- name: Restore cache
  id: restore-step
  uses: xSAVIKx/cloud-cache-action/restore@v1
  with:
    bucket: my-ci-cache-bucket
    key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
    path: build/

Save Only ​

yaml
- name: Save cache
  uses: xSAVIKx/cloud-cache-action/save@v1
  with:
    bucket: my-ci-cache-bucket
    key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
    path: build/

Action Metadata Definitions ​

You can inspect the full action specification files directly in the repository:

action.yml (Click to view unified action definition)
yaml
name: 'Cloud Cache Action'
description: 'Cache artifacts to any S3-compatible storage with 1:1 actions/cache parity and dual-caching'
author: 'Yurii Serhiichuk (https://serhiichuk.dev)'

inputs:
  # Official actions/cache standard inputs
  path:
    description: 'A list of files, directories, and wildcard patterns to cache and restore. Required unless preset is set.'
    required: false
  key:
    description: 'An explicit key for restoring and saving the cache. Required unless preset is set.'
    required: false
  preset:
    description: 'Package manager preset that gives the default path, key and restore-keys: npm, yarn, pnpm, bun, pip, poetry, uv, gradle, maven, cargo, go. The key is <runner.os>-<preset>-<hash of the lockfiles>. An explicit path, key or restore-keys input replaces that one value.'
    required: false
  restore-keys:
    description: 'An ordered multiline string listing the prefix-matched keys, used for restoring stale cache if no cache hit occurred for key'
    required: false
  upload-chunk-size:
    description: 'The chunk size used to split up large files during upload, in bytes (5 MiB to 128 MiB; default 64 MiB, as in actions/cache)'
    required: false
  upload-concurrency:
    description: 'How many multipart upload parts are sent at once (1-32; default 8)'
    required: false
  compression-level:
    description: 'Compression level for saving: 1-19 for zstd, 1-9 for gzip. Unset uses the tool default (zstd 3). Lower is faster and larger.'
    required: false
  enableCrossOsArchive:
    description: 'An optional boolean when enabled, allows windows runners to save or restore caches that can be restored or saved respectively on other platforms'
    default: 'false'
    required: false
  fail-on-cache-miss:
    description: 'Fail the workflow if cache entry is not found'
    default: 'false'
    required: false
  lookup-only:
    description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
    default: 'false'
    required: false
  read-only:
    description: 'Restore cache but do not save cache in post step'
    default: 'false'
    required: false

  # S3 Storage inputs
  bucket:
    description: 'The S3 bucket name where cache archives are stored'
    required: true
  endpoint:
    description: 'Custom S3-compatible endpoint URL (e.g., https://<account_id>.r2.cloudflarestorage.com or https://storage.googleapis.com)'
    required: false
  region:
    description: 'AWS or S3 provider region (e.g., us-east-1, auto, etc.)'
    required: false
  provider:
    description: 'Storage provider preset: aws, r2, gcs, b2, fastly, garage, seaweedfs, minio, rustfs'
    required: false
  access-key:
    description: 'S3 Access Key ID (falls back to AWS_ACCESS_KEY_ID or IAM/OIDC)'
    required: false
  accessKey:
    description: 'Alias for access-key'
    required: false
  secret-key:
    description: 'S3 Secret Access Key (falls back to AWS_SECRET_ACCESS_KEY)'
    required: false
  secretKey:
    description: 'Alias for secret-key'
    required: false
  session-token:
    description: 'S3 Session Token (falls back to AWS_SESSION_TOKEN)'
    required: false
  sessionToken:
    description: 'Alias for session-token'
    required: false
  force-path-style:
    description: 'Force path-style S3 URLs (bucket in path rather than subdomain)'
    required: false
  prefix:
    description: 'Subfolder prefix path within the bucket'
    required: false
    default: ''
  s3-key-pattern:
    description: 'Template for the S3 object key. Default: ${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
    required: false
    default: '${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
  scoped-to-repository:
    description: 'Whether to prefix bucket cache paths with GITHUB_REPOSITORY (default: true)'
    required: false
    default: 'true'
  scoped-to-ref:
    description: 'Save caches under the current Git ref and restore from the current ref, then the pull request base branch, then the default branch (like actions/cache). Set to false to share caches across all refs.'
    required: false
    default: 'true'
  retry:
    description: 'Enable retries with exponential backoff on failure for S3 operations'
    required: false
    default: 'true'
  retry-count:
    description: 'Number of times to retry S3 operations in case of failure'
    required: false
    default: '3'
  use-fallback:
    description: 'Fall back to official GitHub Actions Cache service if S3 operations fail or miss'
    required: false
    default: 'false'
  streaming:
    description: 'Stream archives directly between tar and S3 without a temporary archive file (experimental; default false). Uses less disk and can be faster for large caches, but uploads cannot be retried as a whole and objects carry no integrity checksum.'
    required: false
    default: 'false'
  metadata:
    description: 'User metadata to store on every saved cache object, one key=value per line (stored as x-amz-meta-*; surrounding whitespace is trimmed; keys starting with cloud-cache- are reserved; 2 KB total)'
    required: false
  tags:
    description: 'Object tags to set on every saved cache object, one key=value per line (up to 10; ignored with a warning on providers without object tagging)'
    required: false
  download-concurrency:
    description: 'How many ranged GET requests a restore runs at once when the archive is larger than download-chunk-size (1-32). Set to 1 to download the archive in one request. Default 8.'
    required: false
  download-chunk-size:
    description: 'Bytes per ranged GET request of a parallel download (1 MiB to 128 MiB). Archives no larger than this are downloaded in one request. Default 8388608 (8 MiB).'
    required: false
  transfer-tuning:
    description: 'fixed: use the transfer inputs and their defaults. auto: select the part size and the concurrency per archive, for a restore from its size and for a file-mode save from its size and the provider. A transfer input that you set (upload-concurrency, upload-chunk-size, download-concurrency, download-chunk-size) still wins.'
    required: false
    default: 'fixed'

  # Dual-cache inputs
  dual-cache:
    description: 'Cache to both S3-compatible storage and GitHub Actions Cache simultaneously'
    required: false
    default: 'false'
  restore-priority:
    description: 'Order of cache sources to query during restore: s3-first or github-first'
    required: false
    default: 's3-first'
  dual-cache-strategy:
    description: 'Sync strategy for saving in dual-cache mode: backfill (upload to a tier only if it lacks the key) or skip-on-hit'
    required: false
    default: 'backfill'
  dual-cache-strict:
    description: 'Fail step if either tier encounters an error during dual-cache operations (default: false, fault-tolerant)'
    required: false
    default: 'false'

  # Job summary input
  job-summary:
    description: 'Write a job summary table with the cache keys, hit, source, size, and duration'
    required: false
    default: 'true'

  # Explain input
  explain:
    description: 'Log why the lookup hits or misses (resolved pattern, version, refs searched and every candidate object) before restoring'
    required: false
    default: 'false'

  # Metrics input
  metrics-file:
    description: 'Append one JSON line of timings and sizes for this step to this file (relative to the workspace)'
    required: false
    default: ''

outputs:
  cache-hit:
    description: 'A boolean value to indicate an exact match was found for the primary key'
  cache-primary-key:
    description: 'A resolved cache key for which cache match was attempted'
  cache-matched-key:
    description: 'Key of the cache that was restored, either the primary key or a partial match'
  cache-size:
    description: 'The size of the restored/saved cache archive in bytes'
  cache-storage-provider:
    description: 'The resolved S3-compatible storage provider'
  cache-s3-key:
    description: 'The full S3 object key inside the bucket'
  cache-etag:
    description: 'The ETag checksum of the cache archive in S3'
  cache-metadata:
    description: 'JSON object of the restored object''s user metadata (excluding cloud-cache-* keys); {} when none, on a GitHub-tier hit, or with lookup-only: true'
  cache-hit-source:
    description: 'The cache tier that serviced the restore hit: s3, github, or none'
  cache-saved-sources:
    description: 'Comma-separated list of cache tiers successfully saved to: s3, github, or s3,github'
  cache-restore-duration-ms:
    description: 'Wall-clock milliseconds the restore step took; 0 when it did not complete'
  cache-save-duration-ms:
    description: 'Wall-clock milliseconds the save step took; 0 when it did not complete'
  cache-transfer-duration-ms:
    description: 'Milliseconds spent on the S3 download or upload alone; 0 when nothing was transferred'
  cache-bytes:
    description: 'The size in bytes of the cache archive restored or saved; 0 when none was'

runs:
  using: 'node24'
  main: 'dist/restore/index.js'
  post: 'dist/save/index.js'
  post-if: 'success()'

branding:
  icon: 'cloud'
  color: 'blue'
restore/action.yml (Click to view restore action definition)
yaml
name: 'Restore Cache (S3-Compatible)'
description: 'Restore Cache artifacts from any S3-compatible storage with actions/cache parity'
author: 'Yurii Serhiichuk (https://serhiichuk.dev)'

inputs:
  path:
    description: 'A list of files, directories, and wildcard patterns to restore. Required unless preset is set.'
    required: false
  key:
    description: 'An explicit key for restoring the cache. Required unless preset is set.'
    required: false
  preset:
    description: 'Package manager preset that gives the default path, key and restore-keys: npm, yarn, pnpm, bun, pip, poetry, uv, gradle, maven, cargo, go. The key is <runner.os>-<preset>-<hash of the lockfiles>. An explicit path, key or restore-keys input replaces that one value.'
    required: false
  restore-keys:
    description: 'An ordered multiline string listing the prefix-matched keys, used for restoring stale cache if no cache hit occurred for key'
    required: false
  enableCrossOsArchive:
    description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms'
    default: 'false'
    required: false
  fail-on-cache-miss:
    description: 'Fail the workflow if cache entry is not found'
    default: 'false'
    required: false
  lookup-only:
    description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
    default: 'false'
    required: false

  # S3 Storage inputs
  bucket:
    description: 'The S3 bucket name where cache archives are stored'
    required: true
  endpoint:
    description: 'Custom S3-compatible endpoint URL'
    required: false
  region:
    description: 'AWS or S3 provider region'
    required: false
  provider:
    description: 'Storage provider preset: aws, r2, gcs, b2, fastly, garage, seaweedfs, minio, rustfs'
    required: false
  access-key:
    description: 'S3 Access Key ID'
    required: false
  accessKey:
    description: 'Alias for access-key'
    required: false
  secret-key:
    description: 'S3 Secret Access Key'
    required: false
  secretKey:
    description: 'Alias for secret-key'
    required: false
  session-token:
    description: 'S3 Session Token'
    required: false
  sessionToken:
    description: 'Alias for session-token'
    required: false
  force-path-style:
    description: 'Force path-style S3 URLs'
    required: false
  prefix:
    description: 'Subfolder prefix path within the bucket'
    required: false
    default: ''
  s3-key-pattern:
    description: 'Template for the S3 object key. Default: ${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
    required: false
    default: '${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
  scoped-to-repository:
    description: 'Whether to prefix bucket cache paths with GITHUB_REPOSITORY (default: true)'
    required: false
    default: 'true'
  scoped-to-ref:
    description: 'Save caches under the current Git ref and restore from the current ref, then the pull request base branch, then the default branch (like actions/cache). Set to false to share caches across all refs.'
    required: false
    default: 'true'
  retry:
    description: 'Enable retries with exponential backoff on failure for S3 operations'
    required: false
    default: 'true'
  retry-count:
    description: 'Number of times to retry S3 operations in case of failure'
    required: false
    default: '3'
  use-fallback:
    description: 'Fall back to official GitHub Actions Cache service if S3 operations fail or miss'
    required: false
    default: 'false'
  streaming:
    description: 'Stream archives directly between tar and S3 without a temporary archive file (experimental; default false). Uses less disk and can be faster for large caches, but uploads cannot be retried as a whole and objects carry no integrity checksum.'
    required: false
    default: 'false'
  download-concurrency:
    description: 'How many ranged GET requests a restore runs at once when the archive is larger than download-chunk-size (1-32). Set to 1 to download the archive in one request. Default 8.'
    required: false
  download-chunk-size:
    description: 'Bytes per ranged GET request of a parallel download (1 MiB to 128 MiB). Archives no larger than this are downloaded in one request. Default 8388608 (8 MiB).'
    required: false
  transfer-tuning:
    description: 'fixed: use download-concurrency, download-chunk-size and their defaults. auto: select the part size and the concurrency from the archive size. A download input that you set still wins.'
    required: false
    default: 'fixed'

  # Dual-cache inputs
  dual-cache:
    description: 'Cache to both S3-compatible storage and GitHub Actions Cache simultaneously'
    required: false
    default: 'false'
  restore-priority:
    description: 'Order of cache sources to query during restore: s3-first or github-first'
    required: false
    default: 's3-first'
  dual-cache-strategy:
    description: 'Sync strategy for saving in dual-cache mode: backfill or skip-on-hit'
    required: false
    default: 'backfill'
  dual-cache-strict:
    description: 'Fail step if either tier encounters an error during dual-cache operations'
    required: false
    default: 'false'

  # Job summary input
  job-summary:
    description: 'Write a job summary table with the cache keys, hit, source, size, and duration'
    required: false
    default: 'true'

  # Explain input
  explain:
    description: 'Log why the lookup hits or misses (resolved pattern, version, refs searched and every candidate object) before restoring'
    required: false
    default: 'false'

  # Metrics input
  metrics-file:
    description: 'Append one JSON line of timings and sizes for this step to this file (relative to the workspace)'
    required: false
    default: ''

outputs:
  cache-hit:
    description: 'A boolean value to indicate an exact match was found for the primary key'
  cache-primary-key:
    description: 'A resolved cache key for which cache match was attempted'
  cache-matched-key:
    description: 'Key of the cache that was restored, either the primary key or a partial match'
  cache-size:
    description: 'The size of the restored cache archive in bytes'
  cache-storage-provider:
    description: 'The resolved S3-compatible storage provider'
  cache-s3-key:
    description: 'The full S3 object key inside the bucket'
  cache-etag:
    description: 'The ETag checksum of the cache archive in S3'
  cache-metadata:
    description: 'JSON object of the restored object''s user metadata (excluding cloud-cache-* keys); {} when none, on a GitHub-tier hit, or with lookup-only: true'
  cache-hit-source:
    description: 'The cache tier that serviced the restore hit: s3, github, or none'
  cache-restore-duration-ms:
    description: 'Wall-clock milliseconds the restore step took; 0 when it did not complete'
  cache-transfer-duration-ms:
    description: 'Milliseconds spent on the S3 download or upload alone; 0 when nothing was transferred'
  cache-bytes:
    description: 'The size in bytes of the cache archive restored or saved; 0 when none was'

runs:
  using: 'node24'
  main: '../dist/restore-only/index.js'

branding:
  icon: 'download-cloud'
  color: 'blue'
save/action.yml (Click to view save action definition)
yaml
name: 'Save Cache (S3-Compatible)'
description: 'Save Cache artifacts to any S3-compatible storage with actions/cache parity'
author: 'Yurii Serhiichuk (https://serhiichuk.dev)'

inputs:
  path:
    description: 'A list of files, directories, and wildcard patterns to cache. Required unless preset is set.'
    required: false
  key:
    description: 'An explicit key for saving the cache. Required unless preset is set.'
    required: false
  preset:
    description: 'Package manager preset that gives the default path, key and restore-keys: npm, yarn, pnpm, bun, pip, poetry, uv, gradle, maven, cargo, go. The key is <runner.os>-<preset>-<hash of the lockfiles>. An explicit path, key or restore-keys input replaces that one value.'
    required: false
  upload-chunk-size:
    description: 'The chunk size used to split up large files during upload, in bytes (5 MiB to 128 MiB; default 64 MiB, as in actions/cache)'
    required: false
  upload-concurrency:
    description: 'How many multipart upload parts are sent at once (1-32; default 8)'
    required: false
  transfer-tuning:
    description: 'fixed: use upload-concurrency, upload-chunk-size and their defaults. auto: select the part size and the concurrency of a file-mode save from the archive size and the provider. An upload input that you set still wins. A streaming save keeps the fixed settings.'
    required: false
    default: 'fixed'
  compression-level:
    description: 'Compression level for saving: 1-19 for zstd, 1-9 for gzip. Unset uses the tool default (zstd 3). Lower is faster and larger.'
    required: false
  enableCrossOsArchive:
    description: 'An optional boolean when enabled, allows windows runners to save caches that can be restored on other platforms'
    default: 'false'
    required: false

  # S3 Storage inputs
  bucket:
    description: 'The S3 bucket name where cache archives are stored'
    required: true
  endpoint:
    description: 'Custom S3-compatible endpoint URL'
    required: false
  region:
    description: 'AWS or S3 provider region'
    required: false
  provider:
    description: 'Storage provider preset: aws, r2, gcs, b2, fastly, garage, seaweedfs, minio, rustfs'
    required: false
  access-key:
    description: 'S3 Access Key ID'
    required: false
  accessKey:
    description: 'Alias for access-key'
    required: false
  secret-key:
    description: 'S3 Secret Access Key'
    required: false
  secretKey:
    description: 'Alias for secret-key'
    required: false
  session-token:
    description: 'S3 Session Token'
    required: false
  sessionToken:
    description: 'Alias for session-token'
    required: false
  force-path-style:
    description: 'Force path-style S3 URLs'
    required: false
  prefix:
    description: 'Subfolder prefix path within the bucket'
    required: false
    default: ''
  s3-key-pattern:
    description: 'Template for the S3 object key. Default: ${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
    required: false
    default: '${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
  scoped-to-repository:
    description: 'Whether to prefix bucket cache paths with GITHUB_REPOSITORY (default: true)'
    required: false
    default: 'true'
  scoped-to-ref:
    description: 'Save caches under the current Git ref and restore from the current ref, then the pull request base branch, then the default branch (like actions/cache). Set to false to share caches across all refs.'
    required: false
    default: 'true'
  retry:
    description: 'Enable retries with exponential backoff on failure for S3 operations'
    required: false
    default: 'true'
  retry-count:
    description: 'Number of times to retry S3 operations in case of failure'
    required: false
    default: '3'
  use-fallback:
    description: 'Fall back to official GitHub Actions Cache service if S3 save fails'
    required: false
    default: 'false'
  streaming:
    description: 'Stream archives directly between tar and S3 without a temporary archive file (experimental; default false). Uses less disk and can be faster for large caches, but uploads cannot be retried as a whole and objects carry no integrity checksum.'
    required: false
    default: 'false'
  metadata:
    description: 'User metadata to store on every saved cache object, one key=value per line (stored as x-amz-meta-*; surrounding whitespace is trimmed; keys starting with cloud-cache- are reserved; 2 KB total)'
    required: false
  tags:
    description: 'Object tags to set on every saved cache object, one key=value per line (up to 10; ignored with a warning on providers without object tagging)'
    required: false

  # Dual-cache inputs
  dual-cache:
    description: 'Cache to both S3-compatible storage and GitHub Actions Cache simultaneously'
    required: false
    default: 'false'
  dual-cache-strategy:
    description: 'Sync strategy for saving in dual-cache mode: backfill or skip-on-hit'
    required: false
    default: 'backfill'
  dual-cache-strict:
    description: 'Fail step if either tier encounters an error during dual-cache save'
    required: false
    default: 'false'

  # Job summary input
  job-summary:
    description: 'Write a job summary table with the cache key, saved-to tiers, size, and duration'
    required: false
    default: 'true'

  # Metrics input
  metrics-file:
    description: 'Append one JSON line of timings and sizes for this step to this file (relative to the workspace)'
    required: false
    default: ''

outputs:
  cache-size:
    description: 'The size of the saved cache archive in bytes'
  cache-storage-provider:
    description: 'The resolved S3-compatible storage provider'
  cache-s3-key:
    description: 'The full S3 object key inside the bucket'
  cache-etag:
    description: 'The ETag checksum of the cache archive in S3'
  cache-saved-sources:
    description: 'Comma-separated list of cache tiers successfully saved to: s3, github, or s3,github'
  cache-save-duration-ms:
    description: 'Wall-clock milliseconds the save step took; 0 when it did not complete'
  cache-transfer-duration-ms:
    description: 'Milliseconds spent on the S3 download or upload alone; 0 when nothing was transferred'
  cache-bytes:
    description: 'The size in bytes of the cache archive restored or saved; 0 when none was'

runs:
  using: 'node24'
  main: '../dist/save-only/index.js'

branding:
  icon: 'upload-cloud'
  color: 'blue'
inspect/action.yml (Click to view inspect action definition)
yaml
name: 'Inspect Cache Lookup (S3-Compatible)'
description: 'Explain which cache object a restore would use and why, without restoring'
author: 'Yurii Serhiichuk (https://serhiichuk.dev)'

inputs:
  path:
    description: 'A list of files, directories, and wildcard patterns to restore'
    required: true
  key:
    description: 'An explicit key for restoring the cache'
    required: true
  restore-keys:
    description: 'An ordered multiline string listing the prefix-matched keys, used for restoring stale cache if no cache hit occurred for key'
    required: false
  enableCrossOsArchive:
    description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms'
    default: 'false'
    required: false
  fail-on-cache-miss:
    description: 'Fail the workflow if cache entry is not found'
    default: 'false'
    required: false

  # S3 Storage inputs
  bucket:
    description: 'The S3 bucket name where cache archives are stored'
    required: true
  endpoint:
    description: 'Custom S3-compatible endpoint URL'
    required: false
  region:
    description: 'AWS or S3 provider region'
    required: false
  provider:
    description: 'Storage provider preset: aws, r2, gcs, b2, fastly, garage, seaweedfs, minio, rustfs'
    required: false
  access-key:
    description: 'S3 Access Key ID'
    required: false
  accessKey:
    description: 'Alias for access-key'
    required: false
  secret-key:
    description: 'S3 Secret Access Key'
    required: false
  secretKey:
    description: 'Alias for secret-key'
    required: false
  session-token:
    description: 'S3 Session Token'
    required: false
  sessionToken:
    description: 'Alias for session-token'
    required: false
  force-path-style:
    description: 'Force path-style S3 URLs'
    required: false
  prefix:
    description: 'Subfolder prefix path within the bucket'
    required: false
    default: ''
  s3-key-pattern:
    description: 'Template for the S3 object key. Default: ${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
    required: false
    default: '${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}'
  scoped-to-repository:
    description: 'Whether to prefix bucket cache paths with GITHUB_REPOSITORY (default: true)'
    required: false
    default: 'true'
  scoped-to-ref:
    description: 'Save caches under the current Git ref and restore from the current ref, then the pull request base branch, then the default branch (like actions/cache). Set to false to share caches across all refs.'
    required: false
    default: 'true'
  retry:
    description: 'Enable retries with exponential backoff on failure for S3 operations'
    required: false
    default: 'true'
  retry-count:
    description: 'Number of times to retry S3 operations in case of failure'
    required: false
    default: '3'

  # Tier inputs: they decide the "Tiers:" line of the report, not what inspect lists.
  use-fallback:
    description: 'Fall back to official GitHub Actions Cache service if S3 operations fail or miss'
    required: false
    default: 'false'
  dual-cache:
    description: 'Cache to both S3-compatible storage and GitHub Actions Cache simultaneously'
    required: false
    default: 'false'
  restore-priority:
    description: 'Order of cache sources to query during restore: s3-first or github-first'
    required: false
    default: 's3-first'

  # Job summary input
  job-summary:
    description: 'Write a "Cache lookup explained" job summary section with the full lookup report'
    required: false
    default: 'true'

  max-candidates:
    description: 'How many candidate objects the report shows per search; every object under the prefix is still listed, as a restore does (default 20)'
    required: false
    default: '20'

  # Metrics input
  metrics-file:
    description: 'Append one JSON line of timings and sizes for this step to this file (relative to the workspace)'
    required: false
    default: ''

outputs:
  would-hit:
    description: 'true when a restore with these inputs would find a cache'
  would-match-key:
    description: 'The cache key a restore would match, empty when none'
  would-match-object:
    description: 'The S3 object key a restore would download, empty when none'
  candidate-count:
    description: 'How many objects were listed across every search'
  report:
    description: 'The full lookup report as JSON (truncated to a summary beyond 64 KB)'
  cache-storage-provider:
    description: 'The resolved S3-compatible storage provider'

runs:
  using: 'node24'
  main: '../dist/inspect/index.js'

branding:
  icon: 'search'
  color: 'blue'