From 4a5c0edfd463443ce9d0a608a9120accb9954680 Mon Sep 17 00:00:00 2001 From: Vladimir Antropov Date: Mon, 22 Jun 2026 17:05:35 +0200 Subject: [PATCH 1/4] feat: S3-backed cache-s3 siblings for golang/rust/node-pnpm (Hetzner RGW) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add -s3 sibling cache actions mirroring the existing GitHub-cache ones, following the turborepo/cache-s3 precedent. They restore/save toolchain caches to the Hetzner RadosGW (s3.hz.platforma.bio) via tespkg/actions-cache (pinned to v1.10.2 / e07e2d49) instead of the GitHub-hosted cache. Purpose: on the hz self-hosted runners, drop the shared-hostPath node caches (cross-job contamination surface) and let each job restore/save its own copy from RGW; node-local NVMe is left for the ephemeral per-job workdir only. - Additive only; the existing golang/cache, rust/cache, node/cache-pnpm are untouched, so AWS-runner jobs keep using the GitHub-hosted cache. - Identical key schemes to their siblings. - use-fallback=true → resilient to a transient RGW outage. - Linux only (the hz fleet is Linux/x86). --- actions/golang/cache-s3/action.yaml | 76 ++++++++++++++++++++++++ actions/node/cache-pnpm-s3/action.yaml | 76 ++++++++++++++++++++++++ actions/rust/cache-s3/action.yaml | 80 ++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 actions/golang/cache-s3/action.yaml create mode 100644 actions/node/cache-pnpm-s3/action.yaml create mode 100644 actions/rust/cache-s3/action.yaml diff --git a/actions/golang/cache-s3/action.yaml b/actions/golang/cache-s3/action.yaml new file mode 100644 index 00000000..ef11a3bc --- /dev/null +++ b/actions/golang/cache-s3/action.yaml @@ -0,0 +1,76 @@ +name: Setup S3-backed cache for a Golang application (Hetzner RGW) +author: 'MiLaboratories' +description: | + S3-backed sibling of golang/cache. Restores/saves the Go module + build + caches to an S3 endpoint (the Hetzner RadosGW at s3.hz.platforma.bio) via + tespkg/actions-cache, instead of the GitHub-hosted cache. + + Use on the hz self-hosted runners (hz-rl8-*, hz-ubuntu-dind): the cache is + restored per-job and saved on success, so there is no shared-hostPath cache + on the node and therefore no cross-job contamination. Linux only (the hz + fleet is Linux/x86); macOS/Windows jobs keep using golang/cache. + + The cache key scheme is identical to golang/cache, so switching backends + does not change keys. + +inputs: + cache-version: + description: | + Change this value to 'reset' the cache for a job (forces a cold build). + required: false + default: 'v1' + cache-dependency-hashfiles-path: + description: | + hashFiles() pattern that produces the cache key suffix. + required: false + default: '**/go.work.sum' + + # --- S3 / RadosGW --- + endpoint: + description: "S3 endpoint host, no scheme (split-DNS: resolves to the in-cluster RGW on hz, public elsewhere)." + required: false + default: 's3.hz.platforma.bio' + bucket: + description: "S3 bucket." + required: false + default: 'ci-actions-cache' + region: + description: "S3 region." + required: false + default: 'us-east-1' + insecure: + description: "Use plain HTTP instead of TLS to the endpoint." + required: false + default: 'false' + access-key: + description: "S3 access key (pass from the org secret HZ_CI_CACHE_S3_ACCESS_KEY)." + required: true + secret-key: + description: "S3 secret key (pass from the org secret HZ_CI_CACHE_S3_SECRET_KEY)." + required: true + use-fallback: + description: "Fall back to the GitHub-hosted cache if the S3 endpoint is unreachable." + required: false + default: 'true' + +runs: + using: "composite" + steps: + - name: Cache Golang modules in S3 (RGW) + uses: tespkg/actions-cache@e07e2d4953dc8c020d447363e5064e36d04f3cf9 # v1.10.2 + with: + endpoint: ${{ inputs.endpoint }} + region: ${{ inputs.region }} + bucket: ${{ inputs.bucket }} + insecure: ${{ inputs.insecure }} + accessKey: ${{ inputs.access-key }} + secretKey: ${{ inputs.secret-key }} + use-fallback: ${{ inputs.use-fallback }} + retry: 'true' + retry-count: '3' + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}-${{ hashFiles(inputs.cache-dependency-hashfiles-path) }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}- diff --git a/actions/node/cache-pnpm-s3/action.yaml b/actions/node/cache-pnpm-s3/action.yaml new file mode 100644 index 00000000..700e6c43 --- /dev/null +++ b/actions/node/cache-pnpm-s3/action.yaml @@ -0,0 +1,76 @@ +name: Cache NodeJS PNPM in S3 (Hetzner RGW) +author: 'MiLaboratories' +description: | + S3-backed sibling of node/cache-pnpm. Restores/saves the pnpm content- + addressable store to the Hetzner RadosGW (s3.hz.platforma.bio) via + tespkg/actions-cache, instead of the GitHub-hosted cache. Use on the hz + self-hosted runners. Linux only. + + The pnpm store path is resolved dynamically (`pnpm store path`), so it works + regardless of where the runner image places the store. The cache key scheme + is identical to node/cache-pnpm. + +inputs: + cache-version: + description: "Change this value to 'reset' the cache for a job." + required: false + default: 'v1' + cache-hashfiles-search-path: + description: "hashFiles() pattern for pnpm-lock.yaml." + required: false + default: '**/pnpm-lock.yaml' + + # --- S3 / RadosGW --- + endpoint: + description: "S3 endpoint host, no scheme." + required: false + default: 's3.hz.platforma.bio' + bucket: + description: "S3 bucket." + required: false + default: 'ci-actions-cache' + region: + description: "S3 region." + required: false + default: 'us-east-1' + insecure: + description: "Use plain HTTP instead of TLS to the endpoint." + required: false + default: 'false' + access-key: + description: "S3 access key (pass from the org secret HZ_CI_CACHE_S3_ACCESS_KEY)." + required: true + secret-key: + description: "S3 secret key (pass from the org secret HZ_CI_CACHE_S3_SECRET_KEY)." + required: true + use-fallback: + description: "Fall back to the GitHub-hosted cache if the S3 endpoint is unreachable." + required: false + default: 'true' + +runs: + using: "composite" + steps: + - name: Get pnpm store path + id: pnpm-store-dir + shell: bash + run: echo "dir=$(pnpm store path)" >> ${GITHUB_OUTPUT} + + - name: Cache Node modules in S3 (RGW) + uses: tespkg/actions-cache@e07e2d4953dc8c020d447363e5064e36d04f3cf9 # v1.10.2 + with: + endpoint: ${{ inputs.endpoint }} + region: ${{ inputs.region }} + bucket: ${{ inputs.bucket }} + insecure: ${{ inputs.insecure }} + accessKey: ${{ inputs.access-key }} + secretKey: ${{ inputs.secret-key }} + use-fallback: ${{ inputs.use-fallback }} + retry: 'true' + retry-count: '3' + path: | + ${{ steps.pnpm-store-dir.outputs.dir }} + ~/.pnpm-store + key: ${{ runner.os }}-${{ runner.arch }}-cache-pnpm-${{ inputs.cache-version }}-genericnodejs-${{ hashFiles(inputs.cache-hashfiles-search-path) }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-cache-pnpm-${{ inputs.cache-version }}-genericnodejs- diff --git a/actions/rust/cache-s3/action.yaml b/actions/rust/cache-s3/action.yaml new file mode 100644 index 00000000..0a0bad55 --- /dev/null +++ b/actions/rust/cache-s3/action.yaml @@ -0,0 +1,80 @@ +name: Setup S3-backed cache for a Rust application (Hetzner RGW) +author: 'MiLaboratories' +description: | + S3-backed sibling of rust/cache. Restores/saves the Cargo registry/git caches + and the target/ dir to the Hetzner RadosGW (s3.hz.platforma.bio) via + tespkg/actions-cache, instead of the GitHub-hosted cache. Use on the hz + self-hosted runners. Linux only. + + NOTE: the hz-rl8 runner image bakes CARGO_HOME=/opt/rust/cargo (the rustup + install lives there), so the Cargo registry cache lands under that path, not + ~/.cargo. Pass cargo-home accordingly when consuming on rl8. + + The cache key scheme is identical to rust/cache. + +inputs: + cache-version: + description: "Change this value to 'reset' the cache for a job." + required: false + default: 'v1' + cache-hashfiles-search-path: + description: "hashFiles() pattern for Cargo.lock." + required: false + default: '**/Cargo.lock' + cargo-home: + description: "CARGO_HOME path (hz-rl8 image bakes /opt/rust/cargo; default assumes ~/.cargo)." + required: false + default: '~/.cargo' + + # --- S3 / RadosGW --- + endpoint: + description: "S3 endpoint host, no scheme." + required: false + default: 's3.hz.platforma.bio' + bucket: + description: "S3 bucket." + required: false + default: 'ci-actions-cache' + region: + description: "S3 region." + required: false + default: 'us-east-1' + insecure: + description: "Use plain HTTP instead of TLS to the endpoint." + required: false + default: 'false' + access-key: + description: "S3 access key (pass from the org secret HZ_CI_CACHE_S3_ACCESS_KEY)." + required: true + secret-key: + description: "S3 secret key (pass from the org secret HZ_CI_CACHE_S3_SECRET_KEY)." + required: true + use-fallback: + description: "Fall back to the GitHub-hosted cache if the S3 endpoint is unreachable." + required: false + default: 'true' + +runs: + using: "composite" + steps: + - name: Cache Rust Cargo modules in S3 (RGW) + uses: tespkg/actions-cache@e07e2d4953dc8c020d447363e5064e36d04f3cf9 # v1.10.2 + with: + endpoint: ${{ inputs.endpoint }} + region: ${{ inputs.region }} + bucket: ${{ inputs.bucket }} + insecure: ${{ inputs.insecure }} + accessKey: ${{ inputs.access-key }} + secretKey: ${{ inputs.secret-key }} + use-fallback: ${{ inputs.use-fallback }} + retry: 'true' + retry-count: '3' + path: | + ${{ inputs.cargo-home }}/bin/ + ${{ inputs.cargo-home }}/registry/index/ + ${{ inputs.cargo-home }}/registry/cache/ + ${{ inputs.cargo-home }}/git/db/ + target/ + key: ${{ runner.os }}-${{ runner.arch }}-cache-rust-${{ inputs.cache-version }}-${{ hashFiles(inputs.cache-hashfiles-search-path) }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-cache-rust-${{ inputs.cache-version }}- From 9cd6d6065e277fee66a3363d99a46930346b5529 Mon Sep 17 00:00:00 2001 From: Vladimir Antropov Date: Mon, 22 Jun 2026 23:30:14 +0200 Subject: [PATCH 2/4] fix(golang/cache-s3): cache only go-build, not the read-only module cache tespkg/actions-cache extracts with 'tar --keep-old-files' (unlike stock actions/cache which overwrites), so the read-only (0444) Go module cache trips 'Cannot open: File exists'. Cache only ~/.cache/go-build (the compile-time win); modules repopulate from GOPROXY. --- actions/golang/cache-s3/action.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/actions/golang/cache-s3/action.yaml b/actions/golang/cache-s3/action.yaml index ef11a3bc..d90172c6 100644 --- a/actions/golang/cache-s3/action.yaml +++ b/actions/golang/cache-s3/action.yaml @@ -68,9 +68,14 @@ runs: use-fallback: ${{ inputs.use-fallback }} retry: 'true' retry-count: '3' + # Only the build cache — NOT ~/go/pkg/mod. The module cache is read-only + # (0444) and tespkg/actions-cache extracts with `tar --keep-old-files`, + # which fails ("Cannot open: File exists") on the module cache; the build + # cache is the real compile-time win, and modules repopulate from + # GOPROXY (use a cached-only proxy). (Stock actions/cache overwrote, so + # this only bites the S3-backed action.) path: | ~/.cache/go-build - ~/go/pkg/mod key: ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}-${{ hashFiles(inputs.cache-dependency-hashfiles-path) }} restore-keys: | ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}- From 38ef53c781e2f40292aa5d1156ec1193164d27e7 Mon Sep 17 00:00:00 2001 From: Vladimir Antropov Date: Tue, 23 Jun 2026 01:26:06 +0200 Subject: [PATCH 3/4] feat(golang/cache,prepare): s3 cache backend for self-hosted runners cache-backend=s3 routes the Go build cache to an S3/RGW bucket (tespkg) instead of the Azure-backed GitHub Actions cache, which self-hosted hz runners can't reach. Linux-only, go-build only (modules repopulate from GOPROXY). Default stays 'github' so other repos/runners are unchanged. prepare threads the new inputs through and pins golang/cache to this branch (folds to @v4 on merge). --- actions/golang/cache/action.yaml | 57 +++++++++++++++++++++++++++++- actions/golang/prepare/action.yaml | 40 ++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/actions/golang/cache/action.yaml b/actions/golang/cache/action.yaml index bf27d655..ebe88fcc 100644 --- a/actions/golang/cache/action.yaml +++ b/actions/golang/cache/action.yaml @@ -29,12 +29,67 @@ inputs: required: false default: 'true' + cache-backend: + description: | + Where to store the cache: 'github' (actions/cache, the default) or 's3' + (an S3/RGW bucket via tespkg/actions-cache). Use 's3' on self-hosted + runners that can't reach the Azure-backed GitHub Actions cache. The s3 + backend is Linux-only and caches only ~/.cache/go-build (the module cache + ~/go/pkg/mod is read-only and tespkg's tar --keep-old-files chokes on it; + modules repopulate from GOPROXY). + required: false + default: 'github' + s3-endpoint: + description: "S3 endpoint host/URL (cache-backend=s3)." + required: false + default: '' + s3-bucket: + description: "S3 bucket (cache-backend=s3)." + required: false + default: '' + s3-region: + description: "S3 region (cache-backend=s3)." + required: false + default: 'us-east-1' + s3-insecure: + description: "Use plain HTTP to the S3 endpoint (cache-backend=s3)." + required: false + default: 'false' + s3-access-key: + description: "S3 access key (cache-backend=s3; pass a masked secret)." + required: false + default: '' + s3-secret-key: + description: "S3 secret key (cache-backend=s3; pass a masked secret)." + required: false + default: '' + runs: using: "composite" steps: + - name: Cache Golang modules on Linux (S3/RGW) + if: runner.os == 'Linux' && inputs.cache-backend == 's3' + uses: tespkg/actions-cache@e07e2d4953dc8c020d447363e5064e36d04f3cf9 # v1.10.2 + with: + endpoint: ${{ inputs.s3-endpoint }} + region: ${{ inputs.s3-region }} + bucket: ${{ inputs.s3-bucket }} + insecure: ${{ inputs.s3-insecure }} + accessKey: ${{ inputs.s3-access-key }} + secretKey: ${{ inputs.s3-secret-key }} + use-fallback: 'false' + retry: 'true' + retry-count: '3' + # go-build only — see cache-backend note above. + path: | + ~/.cache/go-build + key: ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}-${{ hashFiles(inputs.cache-dependency-hashfiles-path) }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}- + - name: Cache Golang modules on Linux - if: runner.os == 'Linux' + if: runner.os == 'Linux' && inputs.cache-backend != 's3' uses: actions/cache@v4 with: save-always: ${{ inputs.cache-save-always }} diff --git a/actions/golang/prepare/action.yaml b/actions/golang/prepare/action.yaml index 3e4bab86..91a20af2 100644 --- a/actions/golang/prepare/action.yaml +++ b/actions/golang/prepare/action.yaml @@ -48,6 +48,35 @@ inputs: required: false default: 'true' + cache-backend: + description: "Cache backend: 'github' (default) or 's3'. Passed to golang/cache." + required: false + default: 'github' + s3-endpoint: + description: "S3 endpoint (cache-backend=s3)." + required: false + default: '' + s3-bucket: + description: "S3 bucket (cache-backend=s3)." + required: false + default: '' + s3-region: + description: "S3 region (cache-backend=s3)." + required: false + default: 'us-east-1' + s3-insecure: + description: "Plain HTTP to the S3 endpoint (cache-backend=s3)." + required: false + default: 'false' + s3-access-key: + description: "S3 access key (cache-backend=s3; masked secret)." + required: false + default: '' + s3-secret-key: + description: "S3 secret key (cache-backend=s3; masked secret)." + required: false + default: '' + runs: using: "composite" @@ -60,8 +89,17 @@ runs: cache: ${{ inputs.cache-enabled-in-setup-go }} - name: Setup Cache for Golang project - uses: milaboratory/github-ci/actions/golang/cache@v4 + # Pinned to the hz-s3-cache branch so the s3 backend is available; folds + # back to @v4 on merge. + uses: milaboratory/github-ci/actions/golang/cache@feat/hz-s3-cache-actions with: cache-version: ${{ inputs.cache-version }} cache-dependency-hashfiles-path: ${{ inputs.cache-dependency-hashfiles-path }} cache-save-always: ${{ inputs.cache-save-always }} + cache-backend: ${{ inputs.cache-backend }} + s3-endpoint: ${{ inputs.s3-endpoint }} + s3-bucket: ${{ inputs.s3-bucket }} + s3-region: ${{ inputs.s3-region }} + s3-insecure: ${{ inputs.s3-insecure }} + s3-access-key: ${{ inputs.s3-access-key }} + s3-secret-key: ${{ inputs.s3-secret-key }} From 7b318f1361a1680a76c06ffdc957c0709cc7dfe2 Mon Sep 17 00:00:00 2001 From: Vladimir Antropov Date: Tue, 23 Jun 2026 12:49:35 +0200 Subject: [PATCH 4/4] revert(golang/cache): drop cache-backend flag; add cache-enabled toggle to prepare Consolidate on the golang/cache-s3 sibling action for the s3/RGW Go cache (consistent with node/cache-pnpm-s3 + rust/cache-s3). golang/cache returns to its original GitHub actions/cache form; golang/prepare gains cache-enabled (default true) so a caller can skip the built-in GitHub cache and use the golang/cache-s3 sibling instead (no double-cache). --- actions/golang/cache/action.yaml | 57 +----------------------------- actions/golang/prepare/action.yaml | 46 +++++------------------- 2 files changed, 10 insertions(+), 93 deletions(-) diff --git a/actions/golang/cache/action.yaml b/actions/golang/cache/action.yaml index ebe88fcc..bf27d655 100644 --- a/actions/golang/cache/action.yaml +++ b/actions/golang/cache/action.yaml @@ -29,67 +29,12 @@ inputs: required: false default: 'true' - cache-backend: - description: | - Where to store the cache: 'github' (actions/cache, the default) or 's3' - (an S3/RGW bucket via tespkg/actions-cache). Use 's3' on self-hosted - runners that can't reach the Azure-backed GitHub Actions cache. The s3 - backend is Linux-only and caches only ~/.cache/go-build (the module cache - ~/go/pkg/mod is read-only and tespkg's tar --keep-old-files chokes on it; - modules repopulate from GOPROXY). - required: false - default: 'github' - s3-endpoint: - description: "S3 endpoint host/URL (cache-backend=s3)." - required: false - default: '' - s3-bucket: - description: "S3 bucket (cache-backend=s3)." - required: false - default: '' - s3-region: - description: "S3 region (cache-backend=s3)." - required: false - default: 'us-east-1' - s3-insecure: - description: "Use plain HTTP to the S3 endpoint (cache-backend=s3)." - required: false - default: 'false' - s3-access-key: - description: "S3 access key (cache-backend=s3; pass a masked secret)." - required: false - default: '' - s3-secret-key: - description: "S3 secret key (cache-backend=s3; pass a masked secret)." - required: false - default: '' - runs: using: "composite" steps: - - name: Cache Golang modules on Linux (S3/RGW) - if: runner.os == 'Linux' && inputs.cache-backend == 's3' - uses: tespkg/actions-cache@e07e2d4953dc8c020d447363e5064e36d04f3cf9 # v1.10.2 - with: - endpoint: ${{ inputs.s3-endpoint }} - region: ${{ inputs.s3-region }} - bucket: ${{ inputs.s3-bucket }} - insecure: ${{ inputs.s3-insecure }} - accessKey: ${{ inputs.s3-access-key }} - secretKey: ${{ inputs.s3-secret-key }} - use-fallback: 'false' - retry: 'true' - retry-count: '3' - # go-build only — see cache-backend note above. - path: | - ~/.cache/go-build - key: ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}-${{ hashFiles(inputs.cache-dependency-hashfiles-path) }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-cache-go-${{ inputs.cache-version }}- - - name: Cache Golang modules on Linux - if: runner.os == 'Linux' && inputs.cache-backend != 's3' + if: runner.os == 'Linux' uses: actions/cache@v4 with: save-always: ${{ inputs.cache-save-always }} diff --git a/actions/golang/prepare/action.yaml b/actions/golang/prepare/action.yaml index 91a20af2..2e174997 100644 --- a/actions/golang/prepare/action.yaml +++ b/actions/golang/prepare/action.yaml @@ -48,34 +48,14 @@ inputs: required: false default: 'true' - cache-backend: - description: "Cache backend: 'github' (default) or 's3'. Passed to golang/cache." - required: false - default: 'github' - s3-endpoint: - description: "S3 endpoint (cache-backend=s3)." - required: false - default: '' - s3-bucket: - description: "S3 bucket (cache-backend=s3)." - required: false - default: '' - s3-region: - description: "S3 region (cache-backend=s3)." - required: false - default: 'us-east-1' - s3-insecure: - description: "Plain HTTP to the S3 endpoint (cache-backend=s3)." - required: false - default: 'false' - s3-access-key: - description: "S3 access key (cache-backend=s3; masked secret)." - required: false - default: '' - s3-secret-key: - description: "S3 secret key (cache-backend=s3; masked secret)." + cache-enabled: + description: | + Run the built-in golang/cache step (GitHub actions/cache). Set to 'false' + to skip it — e.g. when the caller provides its own cache (the s3/RGW + golang/cache-s3 sibling on self-hosted runners), to avoid two caches + fighting over the same paths. required: false - default: '' + default: 'true' runs: using: "composite" @@ -89,17 +69,9 @@ runs: cache: ${{ inputs.cache-enabled-in-setup-go }} - name: Setup Cache for Golang project - # Pinned to the hz-s3-cache branch so the s3 backend is available; folds - # back to @v4 on merge. - uses: milaboratory/github-ci/actions/golang/cache@feat/hz-s3-cache-actions + if: inputs.cache-enabled == 'true' + uses: milaboratory/github-ci/actions/golang/cache@v4 with: cache-version: ${{ inputs.cache-version }} cache-dependency-hashfiles-path: ${{ inputs.cache-dependency-hashfiles-path }} cache-save-always: ${{ inputs.cache-save-always }} - cache-backend: ${{ inputs.cache-backend }} - s3-endpoint: ${{ inputs.s3-endpoint }} - s3-bucket: ${{ inputs.s3-bucket }} - s3-region: ${{ inputs.s3-region }} - s3-insecure: ${{ inputs.s3-insecure }} - s3-access-key: ${{ inputs.s3-access-key }} - s3-secret-key: ${{ inputs.s3-secret-key }}