Changelog
All notable changes to Cloud Cache Action are documented here.
This project adheres to Semantic Versioning. The floating v1 tag always points at the latest v1.x.y release. From v1.9.0 on, release-please writes the new entries from the Conventional Commit messages on main. Entries up to v1.9.0 follow Keep a Changelog.
1.9.1 (2026-10-25)
Fixed
- add connection and idle timeouts to the S3 client and log each retry (01334bf)
- name the S3 call, the status and a fix in S3 access errors (305c14e)
- S3 client timeouts and clear access error messages (b4fb9bf)
- say that no cache was restored when a tier failed (da33171)
- stop a stalled S3 response body after the socket timeout (8bc67b2)
1.9.0 - 2026-10-24
Faster uploads to AWS S3, opt-in transfer tuning, and phase timings in the log. Object keys and archive bytes do not change, so caches saved by v1.1 to v1.8 stay valid.
Added
- A timing line for each restore and save. After an S3 download or upload, the step logs one line with the time of each phase and the transfer rate, for example
Timings: lookup 0.24 s, download 5.31 s (35.9 MiB/s), extract 2.41 s, total 8.12 s. The metrics line and themetrics-fileget the same phases aslookupDurationMs,archiveDurationMsandextractDurationMs. The archive and extract phases appear only when the step writes or reads a temporary archive file. A streaming step that falls back to file mode reports them too.totalis the whole step, including the GitHub tier in dual-cache mode. The transfer time and the rate include retries. A save that uploads and then finds that another job saved first ends the line with(another job saved first). - A log line when a streaming save may not tag its object. A streaming save attaches its sha256 as a tag. When the credentials do not have
s3:PutObjectTagging, the save copies the whole object instead, which took about 7.5 s for 512 MiB on AWS S3 in a test. The step now logs this and names the missing permission. Before, only a debug line showed it. transfer-tuning: auto, an opt-in mode that selects the transfer settings. A restore and a file-mode save select the part size and the concurrency from the archive size. A save also uses the provider. For example, a restore uses 16 × 16 MiB, and an upload of more than 256 MiB uses 16 × 32 MiB. The bytes in flight stay within about a quarter of the usable memory, including a container limit, and within 1 GiB. A transfer input that the workflow sets still wins for that field. A streaming save keeps the fixed settings, because it does not know the archive size in advance. The default isfixed, so nothing changes without the input. The rule values come from a size sweep of 128 MiB, 512 MiB and 2 GiB on AWS S3, Cloudflare R2 and Google Cloud Storage.- A Benchmarks page on the website. It compares the defaults, other fixed settings and
transfer-tuning: autoper provider and per cache size.tests/ci/benchmarkPage.tswrites it from the artifacts of a benchmark run. - The benchmark measures several sizes in one run. The workflow input
size-mbtakes a comma-separated list, and the newmatrix: sweepinput runs more part sizes and concurrencies.
Changed
- The transfer inputs have no default in
action.yml.upload-concurrency,download-concurrencyanddownload-chunk-sizenow name their default in the description, and the code applies the same values as before. This letstransfer-tuning: autotell a value that the workflow set from an unset one. - Uploads to AWS S3 no longer compute a CRC32 for each part. The AWS SDK computed this checksum on the main thread, which took about 100 ms for each 64 MiB part on a test host. The action now sends a checksum only when an S3 operation requires one, as it already did for every other provider. The sha256 integrity check on restore stays, and TLS protects the bytes in transit. Downloads from AWS also no longer validate the CRC that the SDK can check on a whole object, because the sha256 check covers the same bytes. Caution: an AWS bucket with an Object Lock default retention requires a checksum on each upload, so saves to it now fail with
400 InvalidRequest. The log can then also say that the server refusedUNSIGNED-PAYLOAD. SetCLOUD_CACHE_SDK_CHECKSUMS=truein the job environment to send the SDK checksums again.
Fixed
- Streaming saves to Google Cloud Storage keep their checksum again. GCS has no tagging API, so a streaming save attaches its sha256 with a copy of the object onto itself. The copy was guarded with
CopySourceIfMatch, which GCS rejects with400 InvalidArgumentfor the ETag of a multipart upload. Every streaming save larger than one part therefore loggedcould not attach metadata: Invalid argument.and kept no checksum and no usermetadata. On GCS the copy now names the generation that the upload wrote as its source version. A copy after another job replaced the object finds no source and fails, as before.
1.8.0 - 2026-10-22
Faster saves and restores. Object keys, metadata and archive bytes do not change, so caches saved by v1.1 to v1.7 stay valid.
Changed
- Upload parts skip the second hash pass over HTTPS. Uploads to AWS S3 and to any
https://endpoint now sendx-amz-content-sha256: UNSIGNED-PAYLOADonUploadPartandPutObjectrequests. The AWS SDK no longer hashes each part again for the request signature. The sha256 integrity check on restore stays, and TLS protects the bytes in transit. Plainhttp://endpoints keep the signed payload. SetCLOUD_CACHE_UNSIGNED_PAYLOAD=falseto turn the change off, orCLOUD_CACHE_UNSIGNED_PAYLOAD=trueto turn it on for anhttp://endpoint. If a server refuses the header, the action sends that request again with a signed payload and logs one line. - A file-mode save hashes the archive while tar writes it. Tar now writes the archive to stdout, and the action computes the sha256 on the way to the temporary file. The save no longer reads the whole archive again before the upload. The archive bytes and the stored sha256 do not change. If tar fails, the action removes the partial file. This applies to GNU tar only. BSD tar (macOS without
gtar, andtar.exein System32 on Windows) writes the file as before, and the action hashes the file after tar. - A file-mode restore hashes the archive while it downloads. For a download in ranged parts, a worker thread computes the sha256 each time the parts at the start of the file are complete. For a download in one request, the action computes the sha256 as the bytes arrive. The restore no longer reads the whole archive again before the integrity check. The check and its error text do not change. If the hash worker fails, the action hashes the file after the download, as before. A streaming restore does not change.
- The second run of the action in a job starts faster. For example, the post step starts faster. Each entry turns on the Node.js V8 compile cache. The cache directory is
cloud-cache-action-v8underRUNNER_TEMP, or under the system temp directory whenRUNNER_TEMPis not set. If the cache cannot start, the step runs without it. The first run in a job does not start faster. A job that uses S3 only no longer loads the GitHub Actions Cache code (@azure/*and@protobuf-ts/*). The action loads this code only when a run uses the GitHub tier. The bundle indist/libis now split into three files.
1.7.0 - 2026-10-22
One default changes: the S3 tier now obeys ACTIONS_CACHE_MODE, so S3-only jobs on pull_request_target, issue_comment and workflow_run no longer save by default (see Changed). Caches saved by v1.1 to v1.6 stay valid.
Added
presetinput. Setpreset: npm(oryarn,pnpm,bun,pip,poetry,uv,gradle,maven,cargo,go) instead ofpath,keyandrestore-keys, like thecache:input ofactions/setup-node. The preset gives the download cache of the package manager for the runner OS, the key<runner.os>-<preset>-<hash of the lockfiles>and the restore key<runner.os>-<preset>-. The hash comes fromhashFilesof@actions/glob, the function thathashFiles()in a workflow uses. An explicitpath,keyorrestore-keysinput replaces that one value. With no lockfile, the step fails and names the globs. The post step saves with the key and the paths that the restore step resolved. The main action and therestoreandsavesub-actions take the input. See the Cache Presets guide.ACTIONS_CACHE_MODEsupport. The S3 tier now obeys theACTIONS_CACHE_MODEenvironment variable in the same way as@actions/cache6.2.0 (and the 5.2.0 backport) does for GitHub Actions Cache. GitHub sets the variable from thecache-modeworkflow key.noneskips the restore and the save.readskips the save.write-onlyskips the restore.write, an unset variable and an unknown value change nothing. The action trims the value and ignores its case, as the toolkit does. A skip logs one info line that names the variable. The metrics line and the job summary show the step as skipped, with the reason. A skip never fails the step, also in strict dual-cache mode. A skipped restore setscache-hittofalse, andfail-on-cache-miss: truethen fails the step, as inactions/cache.read-only: truestill skips the save in every mode. See Parity with actions/cache.Security guide. A new Security page explains the cache trust boundary and what the action does and does not protect. It covers fork pull requests,
pull_request_targetand the read-only credential pattern. It lists each S3 call and the feature that sends it, and it gives least-privilege policies for AWS S3 (read-write, read-only and prune), Cloudflare R2, Google Cloud Storage, MinIO and RustFS. The AWS and MinIO provider pages now link to it.keep-latestandmax-total-sizeprune inputs.keep-latestkeeps the N most recently modified archives of each ref, even when they are older thanolder-than-days. Alone, it deletes every other archive of the ref.max-total-sizeis a budget in bytes: after the other rules, prune deletes the oldest remaining archives in the scope until the total fits.older-than-daysis now optional, but the step fails when none of the four rule inputs (older-than-days,keep-latest,max-total-sizeandabort-uploads-older-than-hours) is set. Each log line names the rule that selected the archive.max-total-size: 0is allowed. It deletes every archive thatkeep-latestdoes not protect. See the Pruning guide.abort-uploads-older-than-hoursprune input. A save that stops in the middle of a multipart upload leaves an incomplete upload that uses storage on providers with no lifecycle rule. Prune now lists the incomplete uploads in its scope and aborts each one that started more than this many hours ago. Set the limit above the duration of your longest save, because prune aborts a save in progress that is older than the limit.dry-runlists them and aborts none. The input counts as a prune rule on its own: with only this input set, prune aborts uploads and deletes no archive. A provider that cannot list uploads gives a warning, not a failure. Measured on the CI servers: RustFS 1.0.0 and Garage 2.4.1 list the uploads with their start time, and prune aborts them. SeaweedFS 4.46 lists them without a start time, so prune aborts none and logs a warning with the count. MinIO lists no upload for a prefix that is not a full object key, so prune finds none. The newaborted-uploadsoutput and theabortedUploadsmetrics field give the count. See the Pruning guide.
Changed
Behaviour change: S3-only jobs on low-trust triggers no longer save by default. The S3 tier now obeys
ACTIONS_CACHE_MODE. GitHub sets it from thecache-modeworkflow key, which is generally available since 2026-09-10. When the workflow does not set the key, the GitHub documentation gives a default ofreadfor low-trust triggers, such aspull_request_target,issue_commentandworkflow_run, that resolve to the default-branch scope. A job on such a trigger then restores but does not save, and logs one info line. Thepull_requestevent is not affected. To save from such a job again, setcache-mode: writeon the job. Only do this when the job cannot run untrusted code.An S3 save that the storage refuses with
AccessDeniednow gives a hint. The warning adds: "The credentials may not be allowed to write to the bucket. Set read-only: true to skip the save and silence this message." A403for wrong credentials (InvalidAccessKeyId,SignatureDoesNotMatch,RequestTimeTooSkewed,ExpiredToken,InvalidToken,TokenRefreshRequired) does not get the hint. The step still only warns. Strict dual-cache mode still fails the step, as for every other save error, and the failure message has the same hint.pathandkeyare optional in the manifests. The main action and therestoreandsavemanifests now declarepathandkeywithrequired: false, because apresetcan give them. With nopresetand nokeyorpath, the restore still fails, and the message now tells you to setpresetor both inputs.One shared bundle in
dist/.dist/held six ncc bundles that each contained the same SDK code. Now ncc builds one bundle,dist/lib/index.js, fromsrc/main.ts. Each action entry (dist/restore,dist/save,dist/restore-only,dist/save-only,dist/pruneanddist/inspect) is a small file that imports this bundle and calls the function of its action. Theaction.ymlpaths do not change.dist/is now 4.9 MB instead of 24 MB, andnpm run buildtakes about 50 seconds instead of about 245 seconds.
Fixed
A dual-cache save where
pathmatches nothing now skips both tiers with the reason "no paths matched". Before, the GitHub tier reported an error, because@actions/cachethrows a plainErrorfor this case and the check expected aValidationError. In strict mode, this failed the step. The S3 tier alone was not affected.On Windows, an S3 tier restore in file mode with Git's GNU
tarno longer fails when the runner account cannot create native symlinks (noSeCreateSymbolicLinkPrivilegeand no Developer Mode). When the only errors areCannot create symlink to ...: Operation not permitted, the action logs a warning and extracts the archive one more time withMSYS=winsymlinks:deepcopy. Symlinks are then restored as copies of their targets. Any other extract error still fails the restore without a retry. The retry does not run withstreaming: true, or for the GitHub tier withdual-cache: true, because@actions/cacheruns its owntar. A symlink whose target is a later symlink in the archive still fails. A copied directory symlink can make the restored tree much larger than the archive.
1.6.0 - 2026-10-22
No breaking changes. Caches saved by v1.1 to v1.5 stay valid. A streamed save now uses object tagging for its checksum, so grant s3:PutObjectTagging and s3:GetObjectTagging (see Changed).
Added
compression-levelinput. Set the zstd (1–19) or gzip (1–9) level a save uses. Unset keeps today's exact command (zstd level 3, or gzip viatar -z), so an existing workflow sees no change. Only saving is affected; restore decodes whichever level a cache was saved with. Measured levels for two real dependency trees, and the reasoning for leaving the default at 3, are in the Performance guide. A newcompressionjob in theTransfer benchmarkworkflow, and acompressionBenchmark.tsscript, reproduce the sweep against your own cache paths.- Faster cache lookups. Refs are still searched one at a time and in the same order as before, so a later branch is never probed before an earlier one is ruled out. Within one ref, the primary-key and restore-key listings now go out together instead of one after another: a total miss with three refs and two restore keys drops from twelve sequential round trips to six, while an exact hit still costs one request. The
explainreport and theinspectsub-action fan out the same way, one ref's listings at a time, and still stop at the first hit, so the rendered report is unchanged.
Changed
- A streamed save now stores its sha256 checksum as an object tag instead of rewriting the whole object, which removes about 8 seconds from a 512 MiB save on Amazon S3. An action older than v1.6 restoring such a cache finds no checksum metadata and skips the integrity check instead of failing, the same way it already does for a v1.1 cache. The object copy is still used when user
metadatais set, when the provider has no tagging API (Garage), when the upload could not use a conditional create, or when the save cannot confirm that it owns the object after the tag. A streamed archive over 5 GiB now gets a checksum for the first time, since a tag has no size limit while the copy was refused above 5 GiB. Withstreaming: true, thetagsinput is capped at 9 instead of 10, because the reserved checksum tag takes one slot.- The save sends one small
HeadObjectafter the tag and compares its ETag with the upload's. A provider that acceptsIf-None-Matchand silently ignores it (Google Cloud Storage) would otherwise let this job tag another job's bytes with this archive's checksum, which a later restore would reject. When the ETags differ, the save removes its checksum tag again, warns, and leaves the other job's cache alone. - When the upload or the
HeadObjectreports no ETag, the save compares a newcloud-cache-writeruser metadata field instead. The upload writes a random id for this save into it, so an object that carries the checksum tag also carries this field. It is not reported incache-metadata. When the save cannot confirm the object either way, it now removes its checksum tag. It then falls back to the copy only when the upload reported an ETag, because only then canCopySourceIfMatchguard the copy. With no upload ETag, the save keeps no checksum and warns, and a failed tag write is handled the same way. A missing checksum only skips the integrity check, but a false checksum would reject a good cache. - The documented IAM policies now grant
s3:GetObjectTaggingands3:PutObjectTagging. A restore that cannot read the tags now warns instead of logging a debug line, because the only other symptom is an integrity check that silently stops running.
- The save sends one small
1.5.1 - 2026-10-21
No breaking changes. This release only updates dependencies.
Changed
- Dependency update to
@actions/cache6.3.0. This version fixes a race on Windows runners. Concurrent restores no longer fight over the tar scratch file.
1.5.0 - 2026-09-22
No breaking changes, and nothing changes for an existing workflow: this release only adds a provider.
Added
- RustFS provider preset.
provider: rustfsselects path-style addressing and theus-east-1default region for RustFS, the Apache-2.0 Rust object store that reached 1.0 in September 2026. The full integration suite runs against RustFS 1.0.0 in CI, next to Garage, SeaweedFS and MinIO. Ranged downloads, multipart uploads, object tagging, user metadata and conditional writes all work, so no feature is disabled for it.- RustFS serves S3 on port 9000, the port MinIO uses, so endpoint sniffing cannot tell the two apart and reports
minio. Both presets resolve to the same settings; setprovider: rustfsto name it in the log.
- RustFS serves S3 on port 9000, the port MinIO uses, so endpoint sniffing cannot tell the two apart and reports
1.4.0 - 2026-09-21
No breaking changes. Caches saved by v1.1, v1.2 and v1.3 stay valid.
Added
- Parallel downloads. A restore now fetches any archive larger than
download-chunk-size(default8388608, 8 MiB) as concurrentRangerequests,download-concurrency(default8, 1–32) at a time, in both file and streaming mode, the same fan-outactions/cacheuses with a larger block (it uses 4 MiB; 8 MiB measured faster on every provider). Each part is retried on its own. Archives no larger than one chunk, and every restore withdownload-concurrency: 1, use a single request as before. A provider that answers a ranged request with the whole object logss3://<bucket>/<key> does not support ranged GET requests; downloading it in one request.and gets the single request. The restore's metrics line reports the part count asdownloadParts. upload-concurrencyinput (default8, 1–32) on the main andsaveactions: how many multipart parts a save sends at once, in both file and streaming mode. It was fixed at 4.- Transfer benchmark workflow (
benchmark.yml, manual) that measures save and restore speed for several transfer settings against the live providers, and a "Transfer Performance" guide with the measured tables and the settings that measured best.
Changed
- Upload defaults follow
actions/cache. The defaultupload-chunk-sizeis 64 MiB instead of 10 MiB and a save sends 8 parts at once instead of 4, so a save may hold up to 512 MiB of parts in memory. Setupload-concurrency: 4andupload-chunk-size: 10485760to keep the v1.3 footprint. Anupload-chunk-sizebelow 5 MiB or above 128 MiB now warns and uses the default; before, a small value was silently replaced by 10 MiB.
1.3.0 - 2026-09-21
No breaking changes. Caches saved by v1.1 and v1.2 stay valid: the key layout, ${version} hashing and archive format are unchanged, and a save that sets none of the new inputs writes the same object as v1.2 did.
Added
- Object metadata and tags. The new
metadatainput stores user metadata (x-amz-meta-*) on every saved cache object and the newtagsinput sets object tags, onekey=valueper line.- Keys starting with
cloud-cache-are reserved, metadata is capped at 2048 bytes in total (including thecloud-cache-sha256entry the save adds) and tags at 10, with S3's tag character set enforced. An invalid value fails the step before any S3 call. - Providers without object tagging log one warning (
s3://<bucket> could not store object tags (<reason>); saved without them.) and save without tags. Garage accepts theTaggingheader but implements no tagging API, so its tags cannot be read back and should be assumed dropped. - With
streaming: truethe metadata is attached by a copy of the object onto itself after the upload; a provider that cannot do that copy costs the metadata, not the cache.
- Keys starting with
cache-metadataoutput. The restored object's user metadata as a JSON object, excludingcloud-cache-*keys;{}when there is none, on a GitHub-tier hit, or withlookup-only: true, which never downloads the object.- A sha256 on streaming saves. A
streaming: truesave now attachescloud-cache-sha256once the stream has finished, so streamed archives get the same integrity check on restore as file-mode ones. The attach is best-effort: if it fails, the save still succeeds and the object simply carries no checksum. explaininput (defaultfalse) on the main andrestoreactions. It logs the whole cache lookup into aCache lookup explainedgroup, and into a job summary section of the same name, before the restore runs: the raw and resolveds3-key-pattern, the${version}hash and the paths, compression method and cross-OS flag it is computed from, the refs and tier order, every listing performed, every candidate object with the version it carries, and a plain-language reason for the outcome. Building the report never fails the step.cloud-cache-action/inspectsub-action that reports which object a restore would use, and why, without restoring. It only lists objects — it downloads nothing and writes nothing.- Outputs:
would-hit,would-match-key,would-match-object,candidate-count,report(the full report as JSON, replaced by a{"truncated":true,…}summary beyond 64 KB) andcache-storage-provider. max-candidates(default20) caps how many objects the report shows per search; every object under the prefix is still listed, as a restore does, and the rest are counted as "… and N more not shown".fail-on-cache-miss: truefails the step when nothing would be restored.- The report lists objects and never
HEADs the exact key, so on a provider with eventually consistent listings a report taken right after a save can say "would miss" where a restore would hit.
- Outputs:
- Metrics outputs.
cache-restore-duration-ms,cache-save-duration-ms,cache-transfer-duration-msandcache-bytesare set on every path, so they are always defined (0when the step did not complete or transferred nothing). metrics-fileinput on the main,restore,save,pruneandinspectactions. Every step writes onecloud-cache-metrics <json>debug line, and appends the same JSON as one line to this file when it is set, resolved relative toGITHUB_WORKSPACE.transferDurationMsmeasures the S3 transfer alone in file mode; withstreaming: trueit covers download-plus-extract, and the line'sstreamingfield tells the two apart.pruneandinspectwrite their line once the step has finished its work, so a step that fails earlier writes none.- Writing the file is best-effort: a failure only warns and never fails the step.
- Documentation: a new "Inspecting Lookups" guide on the documentation site, plus metrics sections in the README and the Getting Started and Pruning guides.
Changed
cache-etagon streaming saves. Withstreaming: truethe output now reports the ETag of the object after its metadata copy, rather than the multipart upload's ETag.- Live cloud CI gating. The Amazon S3, Cloudflare R2 and Google Cloud Storage suites now run on
main, nightly and on manual dispatch, and on a pull request only when it carries thefull-cilabel. Every self-hosted-S3 job (Garage, SeaweedFS, MinIO) still runs on every pull request.
1.2.0 - 2026-09-14
No breaking changes. Caches saved by v1.1 stay valid, and the default save and restore paths still work the same way, apart from the additions below.
Added
cloud-cache-action/prunesub-action that deletes cache archives older thanolder-than-days. It supportsref,scoped-to-ref,scoped-to-repository,prefixanddry-run.- It only deletes objects whose whole key matches the resolved
s3-key-pattern, so other repositories, other refs and non-archive objects under the same listing prefix are never touched. - It refuses to run when a pattern can't be scoped safely.
- An invalid
dry-runvalue fails the step before any S3 call.
- It only deletes objects whose whole key matches the resolved
- Archive integrity. File-mode saves store a sha256 of the archive in the
cloud-cache-sha256object metadata, and restores verify it.- A mismatch counts as a cache miss with a warning.
- It only fails the step when both
dual-cacheanddual-cache-strictaretrue. - Objects without the metadata (any v1.1 cache, or a streamed save) skip the check.
- Safe concurrent saves. Saves send
If-None-Match: *, so when two jobs save the same key, the first one wins and the second keeps the existing cache.- If a provider rejects the condition, the save retries once without it and doesn't send it again for the rest of the run.
- A
409 ConditionalRequestConflictis retried once. - Providers that ignore the condition, such as Garage, keep last-writer-wins.
- Job summary. Restore and save each write a step summary table with the key, hit and source, size and duration. The new
job-summaryinput defaults totrue. - Opt-in streaming (
streaming: true, defaultfalse): pipes tar straight to a multipart upload and downloads straight into tar, with no temporary archive file.- The file-based path stays the default and is unchanged.
- A streamed upload is only completed after tar exits successfully, so a failed tar never leaves a truncated cache.
- It falls back to file mode for BSD tar with zstd on Windows and for rejected conditions.
- Maintenance. Dependabot for npm and GitHub Actions, with minor and patch updates grouped and major updates proposed separately.
- Release workflow (
.github/workflows/release.yml), which runs when a GitHub release is published.- It verifies the tag against
package.jsonand checks thatdistis up to date. - It then moves the major tag, but only for the highest stable release of that major.
- It verifies the tag against
Changed
- The main action,
restoreandsavegain thejob-summaryandstreaminginputs. - A bare
$ref,$key,$prefix,$version,$archive_filenameor$GITHUB_REPOSITORYins3-key-patternis no longer expanded from the environment. It stays literal and logs one warning per name.
Fixed
- A strict dual-cache save where
pathmatches nothing no longer fails the step. The GitHub tier's "Path Validation Error" now counts as a skipped save, as it already did for the S3 tier. - With
scoped-to-repository: falseorscoped-to-ref: false, removing the placeholder no longer leaves a leading, doubled or trailing/. A/is only removed when the placeholder fills a whole path segment, so custom patterns keep their v1.1 keys. - When
CompleteMultipartUploadfails (for example, a lost save race), the multipart upload is aborted instead of leaving billed parts behind. - Errors rewrapped by the restore and save steps keep the original error as
cause.
1.1.0 - 2026-09-13
v1.1 is a correctness release. path now behaves like actions/cache, S3 keys carry the Git ref and a cache version, and dual-cache strict mode actually fails the step.
Breaking changes
- New S3 key layout:
${GITHUB_REPOSITORY}/${prefix}${ref}/${key}/${version}/${archive_filename}.${version}hashes thepathpatterns, the compression method and, on Windows,enableCrossOsArchive.- Caches saved by v1.0 are not found, and are rebuilt once.
- Ref-scoped restores, like actions/cache: the current ref first, then the pull request base branch, then the default branch. The new
scoped-to-refinput (defaulttrue) shares caches across all refs when set tofalse. save-alwaysremoved: it never worked, becausepost-ifcan't read inputs. Use therestoreandsavesub-actions withif: always()instead.dual-cache-strategy: independentremoved: it now logs a warning and behaves asbackfill.dual-cache-strict: truefails the step on tier errors during both restore and save.
Fixed
pathpatterns go through@actions/glob, so~,**and!exclusions work. Archives store paths relative toGITHUB_WORKSPACE.- tar selection matches actions/cache:
- GNU tar on Linux.
gtaror BSD tar on macOS.- Git's GNU tar or System32 tar on Windows, with two-step zstd for BSD tar and
MSYS=winsymlinks:nativestrictfor symlinks. - File names starting with
-can no longer inject tar options.
- Restore-key matching:
- Listing reads every page instead of stopping after 100 keys.
- The primary key is tried as a prefix before
restore-keys. - Keys containing
/or$survive a round trip.
- Failed downloads or extractions count as a cache miss with a warning instead of failing the job.
backfillchecks the other tier first and only uploads to a tier that doesn't already have the key.- Retries:
retry-countsets the SDK's standard retries.- Stream retries only cover network failures the SDK doesn't retry itself.
- Permanent errors such as 403 are no longer retried.
retry-count: 0is honoured.
- S3-compatible providers only receive request checksums when S3 requires them.
- Unknown providers, invalid booleans and enum values, and a lone access or secret key now log a warning.
- The post step reuses the restore step's settings and compression method, so both compute the same object key.
- The
restoreandsavesub-actions use valid Marketplace icons.
Testing
- Unit tests on real temporary directories, real tar round trips, and a contract test that keeps all
action.ymlmanifests in sync with the code. - Integration tests against SeaweedFS, MinIO and Garage.
- CI on Linux, macOS and Windows:
- per-OS round trips and post-step saves
- cross-OS restores
- dual-cache against the live GitHub Actions Cache
- a strict-mode failure check
- actionlint
- A nightly live cross-OS restore on Cloudflare R2.
1.0.0 - 2026-09-11
First stable release. Inputs and outputs are unchanged from 0.1.0.
Added
- Dedicated live provider verification workflows for Amazon S3, Cloudflare R2 and Google Cloud Storage.
- The repository's own CI and documentation deployment use the action.
Changed
- Documentation and examples reference
@v1. - The action description was shortened to fit the Marketplace's 125-character limit.
0.1.0 - 2026-09-11
Initial pre-release.
Added
- Drop-in replacement for
actions/cache:- Inputs
path,key,restore-keys,fail-on-cache-miss,lookup-only,enableCrossOsArchive,read-onlyandsave-always. - Outputs
cache-hit,cache-primary-keyandcache-matched-key. - Runs on Node 24.
- Inputs
- Storage: any S3-compatible storage (AWS S3, Cloudflare R2, Google Cloud Storage, Backblaze B2, Fastly Object Storage, Garage, SeaweedFS, MinIO), with custom endpoints, path-style addressing and session tokens.
- Dual-caching to S3 and the GitHub Actions Cache, with
restore-priority,dual-cache-strategyanddual-cache-strict. - Custom S3 key templates (
s3-key-pattern) withprefixandscoped-to-repository. - zstd compression with a gzip fallback, chunked multipart uploads and retries.
restoreandsavesub-actions.