MinIO S3 Setup
MinIO is a high-performance distributed object store with native S3 API compatibility, widely used for on-premise infrastructure, private enterprise clouds, and local CI testing.
NOTE
Maintenance & Compatibility Notice: While upstream open-source MinIO changed licensing (AGPLv3) and standalone community releases are no longer actively maintained with free public security patches, many development teams and enterprise clusters continue to rely on existing MinIO infrastructure or use ephemeral containers in CI. cloud-cache-action maintains 100% interoperability with all MinIO versions.
Best Practices: Bucket & Cluster Setup
Follow these recommendations when setting up MinIO for CI/CD caching:
1. Bucket Privacy & Quotas
- Bucket Access Policy: Ensure the bucket access policy is strictly private (
none). Never set download or public access policies on cache buckets. - Hard Storage Quotas: CI builds can rapidly generate hundreds of gigabytes of dependency archives. Use the MinIO Client (
mc) to set a hard storage quota on your cache bucket:bash# Set a 100 GB hard limit on the cache bucket mc quota set --hard 100GB myminio/ci-cache
2. Information Lifecycle Management (ILM) Auto-Expiration
MinIO includes native ILM lifecycle management. Configure automatic expiration so old caches are purged automatically:
# Automatically expire and delete cache archives older than 30 days
mc ilm rule add --expire-days 30 myminio/ci-cache
# Automatically abort incomplete multipart uploads after 7 days
mc ilm rule add --expire-delete-marker --abort-incomplete-multipart-upload-days 7 myminio/ci-cache3. Server-Side Encryption
Enable transparent server-side encryption with MinIO-managed keys:
# Enable auto-encryption on the bucket
mc encrypt set sse-s3 myminio/ci-cacheCredentials & Least-Privilege Service Accounts
WARNING
Never use root credentials (MINIO_ROOT_USER / MINIO_ROOT_PASSWORD) in your GitHub Actions workflows. Always create a dedicated Service Account restricted strictly to the CI cache bucket.
1. Create a Restricted Policy (ci-cache-policy.json)
Copy the MinIO policy from the Security guide into ci-cache-policy.json. Change the bucket name and the prefix to match your set-up.
Add the policy to MinIO:
mc admin policy create myminio ci-cache-policy ci-cache-policy.json2. Generate Service Account Credentials
# Create a service account restricted by the policy
mc admin user svcacct add --policy ci-cache-policy myminio ci-runnerMinIO will print the Access Key and Secret Key.
3. Configure GitHub Secrets
Store the credentials in your repository's Settings > Secrets and variables > Actions:
| Secret Name | Description |
|---|---|
MINIO_ACCESS_KEY | MinIO Service Account Access Key |
MINIO_SECRET_KEY | MinIO Service Account Secret Key |
Example 1: GitHub Actions CI with Ephemeral MinIO Container
You can spin up an ephemeral MinIO instance directly inside your GitHub Actions runner job using Docker or Docker Compose for zero-cost, isolated CI caching:
name: Build with Local MinIO Cache
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 1. Start ephemeral MinIO container
- name: Start ephemeral MinIO
run: |
docker run -d --name ci-minio -p 9000:9000 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio:RELEASE.2025-09-07T16-13-09Z server /data
# Wait for MinIO readiness
for i in {1..30}; do
if curl -s http://127.0.0.1:9000/minio/health/live > /dev/null 2>&1; then
echo "MinIO ready."
break
fi
sleep 1
done
# Create cache bucket
docker exec ci-minio mkdir -p /data/ci-cache
# 2. Cache dependencies with Cloud Cache Action
- name: Cache dependencies using local MinIO
uses: xSAVIKx/cloud-cache-action@v1
with:
bucket: ci-cache
endpoint: http://127.0.0.1:9000
access-key: minioadmin
secret-key: minioadmin
force-path-style: true
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
path: ~/.npm
- name: Install dependencies
run: npm ciExample 2: Self-Hosted / On-Prem MinIO Cluster
For organizations hosting a persistent MinIO server or cluster on private infrastructure (e.g., bare-metal or Kubernetes):
- name: Cache dependencies using self-hosted MinIO
uses: xSAVIKx/cloud-cache-action@v1
with:
bucket: ci-cache
endpoint: https://minio.internal.mycompany.com:9000
access-key: ${{ secrets.MINIO_ACCESS_KEY }}
secret-key: ${{ secrets.MINIO_SECRET_KEY }}
force-path-style: true
key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
path: build/TIP
Path-Style Addressing: Always set force-path-style: true for MinIO unless you have configured wildcard DNS subdomains (virtual-host style) for your buckets.
Running MinIO Locally for Integration Testing
You can spin up MinIO locally using the repository's docker-compose.test.yml:
docker compose -f docker-compose.test.yml up -d miniodocker-compose.test.yml (Click to view Compose definition)
name: cloud-cache-test
# Local S3 servers for the integration suite. Credentials match tests/support/s3Server.ts:
# seaweedfs, minio, rustfs: cloudcacheci / cloudcachecisecret
# garage: run `bash tests/ci/start-garage.sh` after `up`, then use the key in that script.
services:
garage:
image: dxflrs/garage:v2.4.1
container_name: cloud-cache-garage
ports:
- '3900:3900' # S3 API
- '3903:3903' # Admin API
volumes:
- ./tests/fixtures/garage.toml:/etc/garage.toml:ro
environment:
- RUST_LOG=info
restart: unless-stopped
seaweedfs:
image: chrislusf/seaweedfs:4.46
container_name: cloud-cache-seaweedfs
ports:
- '8333:8333' # S3 API
volumes:
- ./tests/fixtures/seaweedfs-s3.json:/etc/seaweedfs/s3.json:ro
command: 'server -dir=/data -s3 -s3.port=8333 -s3.config=/etc/seaweedfs/s3.json -volume.max=0 -master.volumeSizeLimitMB=64'
restart: unless-stopped
minio:
# Upstream MinIO no longer publishes community images; CleanStart rebuilds it from source.
image: cleanstart/minio:0.20260330.001845@sha256:d5dc9098ea6239fc7215165c1496fa026abbb9560bdd7a67c4938e9226394839
container_name: cloud-cache-minio
ports:
- '9000:9000'
environment:
- MINIO_ROOT_USER=cloudcacheci
- MINIO_ROOT_PASSWORD=cloudcachecisecret
tmpfs:
- /data:uid=65532,gid=65532
command: 'server /data'
restart: unless-stopped
rustfs:
# RustFS serves S3 on 9000, the same port MinIO uses, so it is published on 9010 here to let
# both run at once. The container runs as uid/gid 10001 and needs /data and /logs writable.
image: rustfs/rustfs:1.0.0
container_name: cloud-cache-rustfs
ports:
- '9010:9000' # S3 API
environment:
- RUSTFS_ACCESS_KEY=cloudcacheci
- RUSTFS_SECRET_KEY=cloudcachecisecret
tmpfs:
- /data:uid=10001,gid=10001
- /logs:uid=10001,gid=10001
restart: unless-stoppedOnce running:
- S3 API:
http://localhost:9000 - Web Console:
http://localhost:9001(Default credentials:minioadmin/minioadmin)
Live CI Dogfooding Workflow
The repository dogfoods MinIO directly inside the continuous integration test suite: .github/workflows/test.yml.
.github/workflows/test.yml (Click to view test workflow)
name: CI Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
env:
REQUIRE_S3: '1'
jobs:
checks:
name: Format, lint, types, dist and docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run format:check
- run: npm run lint
- name: Lint workflows with actionlint
run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12 -color
- run: npm run typecheck
- run: npm run build
- name: Verify dist is clean and tracked
run: |
if [ -n "$(git status --porcelain dist)" ]; then
echo "::error::Uncommitted dist changes detected. Run npm run build and commit."
git status --porcelain dist
exit 1
fi
- run: npm run docs:build
unit:
name: Jest (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- name: Start SeaweedFS
run: node tests/ci/startSeaweedfs.ts
- name: Run Jest (unit, contract and integration)
run: npm test
provider-compat:
name: S3 compatibility (${{ matrix.server }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- server: seaweedfs
endpoint: http://127.0.0.1:8333
region: us-east-1
access-key: cloudcacheci
secret-key: cloudcachecisecret
- server: garage
endpoint: http://127.0.0.1:3900
region: garage
access-key: GK0123456789abcdef01234567
secret-key: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- server: minio
endpoint: http://127.0.0.1:9000
region: us-east-1
access-key: cloudcacheci
secret-key: cloudcachecisecret
# RustFS serves S3 on 9000 like MinIO, so docker-compose.test.yml publishes it on 9010.
- server: rustfs
endpoint: http://127.0.0.1:9010
region: us-east-1
access-key: cloudcacheci
secret-key: cloudcachecisecret
env:
TEST_S3_ENDPOINT: ${{ matrix.endpoint }}
TEST_S3_REGION: ${{ matrix.region }}
TEST_S3_ACCESS_KEY: ${{ matrix.access-key }}
TEST_S3_SECRET_KEY: ${{ matrix.secret-key }}
TEST_S3_PROVIDER: ${{ matrix.server }}
KEY: compat-${{ matrix.server }}-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- name: Start ${{ matrix.server }}
run: docker compose -f docker-compose.test.yml up -d ${{ matrix.server }}
- name: Bootstrap Garage
if: matrix.server == 'garage'
run: bash tests/ci/start-garage.sh
- name: Wait for S3 and create the bucket
run: |
for _ in $(seq 1 60); do
if node tests/support/s3Server.ts; then exit 0; fi
sleep 2
done
docker compose -f docker-compose.test.yml logs ${{ matrix.server }}
exit 1
- name: Integration tests
run: npm run test:integration
- name: Build the fixture tree
id: fixture
run: node tests/support/fixtureTree.ts build "$RUNNER_TEMP/fixture.json" --large-bytes 33554432
- name: Save (save-only)
uses: ./save
with:
key: ${{ env.KEY }}
path: ${{ steps.fixture.outputs.paths }}
bucket: cloud-cache-test
endpoint: ${{ matrix.endpoint }}
region: ${{ matrix.region }}
provider: ${{ matrix.server }}
access-key: ${{ matrix.access-key }}
secret-key: ${{ matrix.secret-key }}
- name: Remove the fixture tree
run: node tests/support/fixtureTree.ts clear
- name: Restore (restore-only)
id: restore
uses: ./restore
with:
key: ${{ env.KEY }}
path: ${{ steps.fixture.outputs.paths }}
fail-on-cache-miss: true
bucket: cloud-cache-test
endpoint: ${{ matrix.endpoint }}
region: ${{ matrix.region }}
provider: ${{ matrix.server }}
access-key: ${{ matrix.access-key }}
secret-key: ${{ matrix.secret-key }}
- name: Verify the restored tree
run: |
test "${{ steps.restore.outputs.cache-hit }}" = "true"
node tests/support/fixtureTree.ts verify "$RUNNER_TEMP/fixture.json"
cli:
name: CLI bundle against RustFS
runs-on: ubuntu-latest
env:
TEST_S3_ENDPOINT: http://127.0.0.1:9010
TEST_S3_REGION: us-east-1
TEST_S3_ACCESS_KEY: cloudcacheci
TEST_S3_SECRET_KEY: cloudcachecisecret
TEST_S3_PROVIDER: rustfs
TEST_CLI: '1'
REQUIRE_S3: '1'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build:cli
- name: Start RustFS
run: docker compose -f docker-compose.test.yml up -d rustfs
- name: Wait for S3 and create the bucket
run: |
for _ in $(seq 1 60); do
if node tests/support/s3Server.ts; then exit 0; fi
sleep 2
done
docker compose -f docker-compose.test.yml logs rustfs
exit 1
- name: Run the CLI integration test
run: npm test -- tests/integration/cli.test.ts --testTimeout=60000
roundtrip:
name: Round trip (${{ matrix.os }}, streaming ${{ matrix.streaming }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
streaming: ['false', 'true']
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: rt-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Build the fixture tree
id: fixture
run: node tests/support/fixtureTree.ts build "$RUNNER_TEMP/fixture.json" --large-bytes 33554432
- name: Save (save-only)
uses: ./save
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-v1
path: ${{ steps.fixture.outputs.paths }}
streaming: ${{ matrix.streaming }}
metrics-file: metrics.jsonl
- name: Remove the fixture tree
run: node tests/support/fixtureTree.ts clear
- name: Restore the exact key
id: exact
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-v1
path: ${{ steps.fixture.outputs.paths }}
fail-on-cache-miss: true
streaming: ${{ matrix.streaming }}
metrics-file: metrics.jsonl
- name: Verify the restored tree
run: |
test "${{ steps.exact.outputs.cache-hit }}" = "true"
node tests/support/fixtureTree.ts verify "$RUNNER_TEMP/fixture.json"
- name: Verify the metrics file has one line per step
run: |
cat metrics.jsonl
test "$(wc -l < metrics.jsonl)" -eq 2
- name: Look up by primary-key prefix
id: prefix
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-v
path: ${{ steps.fixture.outputs.paths }}
lookup-only: true
- name: Look up by restore-keys
id: restore-keys
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-other
restore-keys: ${{ env.KEY }}-v
path: ${{ steps.fixture.outputs.paths }}
lookup-only: true
- name: Look up with a different path list
id: mismatch
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-v1
path: |
${{ steps.fixture.outputs.paths }}
not-in-the-original-list
lookup-only: true
- name: Verify the lookups
run: |
test "${{ steps.prefix.outputs.cache-hit }}" = "false"
test "${{ steps.prefix.outputs.cache-matched-key }}" = "$KEY-v1"
test "${{ steps.restore-keys.outputs.cache-hit }}" = "false"
test "${{ steps.restore-keys.outputs.cache-matched-key }}" = "$KEY-v1"
test "${{ steps.mismatch.outputs.cache-hit }}" = "false"
test -z "${{ steps.mismatch.outputs.cache-matched-key }}"
main-action:
name: Main action post step (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: main-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Create a payload
run: mkdir -p payload && echo "${{ github.run_id }}" > payload/run.txt
- name: After every post step, check that the cache was saved
uses: ./.github/actions/assert-object
with:
key: ${{ env.KEY }}
- name: Cache the payload (restore now, save in the post step)
id: cache
uses: ./
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: payload
- name: Expect a miss on the first attempt
run: test "${{ steps.cache.outputs.cache-hit }}" = "false"
prune:
name: Prune sub-action
runs-on: ubuntu-latest
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: prune-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Create two payloads
run: |
mkdir -p payload-a payload-b
echo "a-${{ github.run_id }}" > payload-a/data.txt
echo "b-${{ github.run_id }}" > payload-b/data.txt
- name: Save the first key
uses: ./save
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-a
path: payload-a
- name: Save the second key
uses: ./save
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}-b
path: payload-b
- name: Dry-run prune of caches older than 1 day
id: prune
uses: ./prune
with:
bucket: cloud-cache-test
older-than-days: 1
dry-run: true
- name: Expect nothing pruned, since the objects are brand new
run: test "${{ steps.prune.outputs.pruned-count }}" = "0"
- name: Confirm both objects still exist
run: |
count=$(node tests/ci/s3Objects.ts count "$GITHUB_REPOSITORY/" "/$KEY-")
test "$count" -eq 2
inspect:
name: Inspect sub-action
runs-on: ubuntu-latest
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: inspect-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Create two payloads
run: |
mkdir -p payload other-dir
echo "payload-${{ github.run_id }}" > payload/data.txt
echo "other-${{ github.run_id }}" > other-dir/data.txt
- name: Save the cache
uses: ./save
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: payload
- name: Inspect the same lookup
id: inspect
uses: ./inspect
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: payload
- name: Expect the lookup to hit the object that was just saved
run: |
test "${{ steps.inspect.outputs.would-hit }}" = "true"
test "${{ steps.inspect.outputs.would-match-key }}" = "$KEY"
test -n "${{ steps.inspect.outputs.would-match-object }}"
- name: Smoke-test the inspect bundle itself
env:
INPUT_BUCKET: cloud-cache-test
INPUT_KEY: ${{ env.KEY }}
INPUT_PATH: payload
GITHUB_OUTPUT: ${{ runner.temp }}/inspect-bundle-output.txt
run: |
: > "$GITHUB_OUTPUT"
node dist/inspect/index.js
cat "$GITHUB_OUTPUT"
# core.setOutput writes "<name><<<delimiter>\n<value>\n<delimiter>".
test "$(awk '/^would-hit<</ { getline; print; exit }' "$GITHUB_OUTPUT")" = "true"
- name: Inspect the same key with different paths
id: inspect-miss
uses: ./inspect
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: other-dir
- name: Expect a miss, since the paths hash to another version
run: |
test "${{ steps.inspect-miss.outputs.would-hit }}" = "false"
test -z "${{ steps.inspect-miss.outputs.would-match-object }}"
dual-cache:
name: Dual-cache with the live GitHub Actions Cache
runs-on: ubuntu-latest
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: dual-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Create a payload
run: mkdir -p payload && echo "${{ github.run_id }}" > payload/run.txt
- name: After the dual-cache post step, check that S3 received the object
uses: ./.github/actions/assert-object
with:
key: ${{ env.KEY }}
- name: Cache the payload in both tiers (restore now, post step saves to S3 and GitHub)
id: cache
uses: ./
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: payload
dual-cache: true
- name: Expect a miss in both tiers on the first attempt
run: test "${{ steps.cache.outputs.cache-hit }}" = "false"
strict-failure:
name: Strict dual-cache save failure
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Create a payload
run: mkdir -p payload && echo "${{ github.run_id }}" > payload/run.txt
- name: Save with a wrong secret in strict dual-cache mode (must fail)
id: strict-save
continue-on-error: true
uses: ./save
with:
key: strict-${{ github.run_id }}-${{ github.run_attempt }}
path: payload
bucket: cloud-cache-test
endpoint: http://127.0.0.1:8333
access-key: cloudcacheci
secret-key: wrong-secret
dual-cache: true
dual-cache-strict: true
- name: Expect the strict save step to have failed
run: test "${{ steps.strict-save.outcome }}" = "failure"
cross-os-seed:
name: Cross-OS seed (Linux)
runs-on: ubuntu-latest
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: xos-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- run: node tests/ci/startSeaweedfs.ts
- name: Build the portable fixture tree
id: fixture
run: node tests/support/fixtureTree.ts build "$RUNNER_TEMP/seed/fixture.json" --portable --large-bytes 8388608
- name: Save with enableCrossOsArchive
uses: ./save
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: ${{ steps.fixture.outputs.paths }}
enableCrossOsArchive: true
- name: Export the saved objects
run: node tests/ci/s3Objects.ts export "$RUNNER_TEMP/seed/objects" "$GITHUB_REPOSITORY/" "/$KEY/"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: cross-os-seed
path: ${{ runner.temp }}/seed
if-no-files-found: error
cross-os-restore:
name: Cross-OS restore (${{ matrix.os }})
needs: cross-os-seed
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
hit-without-cross-os: 'true'
- os: windows-latest
hit-without-cross-os: 'false'
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
env:
AWS_ENDPOINT_URL: http://127.0.0.1:8333
AWS_S3_BUCKET: cloud-cache-test
AWS_ACCESS_KEY_ID: cloudcacheci
AWS_SECRET_ACCESS_KEY: cloudcachecisecret
KEY: xos-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
- run: npm ci --omit=dev --no-audit --no-fund
- name: Require zstd, which the Linux seed used
run: zstd --version
- run: node tests/ci/startSeaweedfs.ts
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: cross-os-seed
path: ${{ runner.temp }}/seed
- name: Import the seeded objects
run: node tests/ci/s3Objects.ts import "$RUNNER_TEMP/seed/objects"
- name: Read the seeded paths
id: seed
run: node tests/support/fixtureTree.ts paths "$RUNNER_TEMP/seed/fixture.json"
- name: Look up without enableCrossOsArchive
id: plain
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: ${{ steps.seed.outputs.paths }}
lookup-only: true
- name: Restore with enableCrossOsArchive
id: cross
uses: ./restore
with:
bucket: cloud-cache-test
key: ${{ env.KEY }}
path: ${{ steps.seed.outputs.paths }}
enableCrossOsArchive: true
fail-on-cache-miss: true
- name: Verify
run: |
test "${{ steps.plain.outputs.cache-hit }}" = "${{ matrix.hit-without-cross-os }}"
test "${{ steps.cross.outputs.cache-hit }}" = "true"
node tests/support/fixtureTree.ts verify "$RUNNER_TEMP/seed/fixture.json"Notes
- Checksums: for this provider, the action sends request checksums only when S3 requires them, because many S3-compatible services reject the CRC checksums recent AWS SDKs send by default. Nothing to configure.
- Expiry: the action never deletes caches. Add a lifecycle rule that expires objects after 30–60 days so old caches do not accumulate.
- Incomplete uploads: MinIO lists no incomplete multipart upload for a prefix that is not a full object key (measured on 2026-10-21 with the CI image). The
pruneinputabort-uploads-older-than-hourstherefore finds no upload and aborts none. Use the--abort-incomplete-multipart-upload-dayslifecycle rule above instead.