ci: optimized github actions caching and workflows

- Updated GitHub Actions workflows and custom actions to optimize caching strategies and runners.
- Configured separate restore and save steps for bun store caching with non-PR restrictions.
- Added darwin release bazel cache seeding and fallback keys for cache restore.
- Removed native-inputs workflow action and disabled PR-side Rust validation.
This commit is contained in:
can1357
2026-07-30 04:56:47 +02:00
parent ccd3bb9565
commit a38a2f25cf
7 changed files with 174 additions and 272 deletions
+65 -47
View File
@@ -1,24 +1,38 @@
name: Warm bazel disk cache
# GitHub-hosted PR runners cannot use the cluster remote cache and only see
# actions/cache entries created on the default branch. Bazel action keys do
# not transfer across runner environments (a kata-produced disk cache misses
# every action on ubuntu-22.04), so seed the disk cache from the same image
# PR jobs run on. Runs the full hosted action set — Rust validation (test,
# clippy, rustfmt) plus the native addons — so one exact-key archive covers
# both rust_validate and native_addons for this source generation. The v3
# key embeds a crates/** source fingerprint, so a native change on main
# means an exact miss: the build seeds from the previous generation via the
# config-scoped prefix and the refreshed archive is saved. An exact hit
# makes every invocation a cache replay and saves nothing.
# GitHub-hosted runners cannot use the cluster remote cache and only see
# actions/cache entries created on the default branch, and bazel action keys
# do not transfer across runner environments (a kata-produced disk cache
# misses every action on a hosted image) — so seed each hosted consumer from
# the same image it runs on:
#
# warm_darwin (macos-*) -> scopes `release-darwin-*`, consumed by the
# release_binary darwin jobs. Releases are the
# only other producers for these scopes and a
# release always changes CONFIG_HASH (version
# bump rewrites Cargo.toml/Cargo.lock), so
# without warming here every release rebuilt the
# darwin addons cold (~40-50 min on
# macos-15-intel — run 30357804722).
# warm_bun (ubuntu-22.04)-> the shared `bun-<os>-<lockhash>` store entry.
# PR jobs restore it but never save (per-PR-ref
# copies of the ~500 MB store once ate ~9.4 GB of
# the 10 GB repo cache quota and evicted every
# bazel archive).
#
# The v3 key embeds a crates/** source fingerprint, so a native change on
# main means an exact miss: the build seeds from a previous generation via
# the prefix/bare restore keys and the refreshed archive is saved. An exact
# hit makes every invocation a cache replay and saves nothing.
#
# Paths mirror the cache-key inputs (bazel-cache action) plus bun.lock — a
# packages/** push cannot change any archive, so it does not trigger a warm.
on:
push:
branches: [main]
paths:
- "packages/**"
- "crates/**"
- "scripts/**"
- "bazel/**"
- "MODULE.bazel"
- "MODULE.bazel.lock"
@@ -30,50 +44,54 @@ on:
- "Cargo.lock"
- "rust-toolchain.toml"
- "rustfmt.toml"
- "bun.lock"
- ".github/**"
workflow_dispatch:
permissions:
contents: read
# No cancel-in-progress: a cancelled warm saves nothing, and the first darwin
# warm on a cold runner takes long enough that back-to-back native pushes
# would cancel it forever. Superseded queued runs still collapse to the
# newest one, and staleness is harmless — consumers seed via prefix keys.
concurrency:
group: bazel-cache-warm
cancel-in-progress: true
cancel-in-progress: false
jobs:
warm:
name: Seed hosted bazel disk cache
# Keeps the release_binary darwin scopes seeded near HEAD so a release's
# bazel build is the version-bump delta, not a cold graph. Scope, runner
# image, and target must stay in lockstep with the release_binary matrix
# in ci.yml — a mismatched scope warms an archive nobody restores.
warm_darwin:
name: "Seed darwin release bazel cache: ${{ matrix.target }}"
strategy:
fail-fast: false
matrix:
include:
- { os: macos-15-intel, target: darwin-x64-baseline, scope: release-darwin-x64 }
- { os: macos-14, target: darwin-arm64, scope: release-darwin-arm64 }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: "1.3"
# scripts/bazel-natives.ts is dependency-free (node builtins only), so
# no `bun install` is needed; the bazel-natives action restores,
# builds, and saves the scope's disk cache.
- uses: ./.github/actions/bazel-natives
with:
targets: ${{ matrix.target }}
cache-scope: ${{ matrix.scope }}
# Produces the shared bun store entry PR jobs restore. bun-install's save
# half only runs on non-PR events, and main CI jobs run on omp-kata (PVC
# store, no GitHub cache), so this is the sole hosted-runner producer.
warm_bun:
name: Seed bun store cache
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- id: cache
uses: ./.github/actions/bazel-cache
with:
scope: linux
# Invocation set mirrors the hosted CI jobs (rust_validate +
# native_addons) so the saved archive serves both.
- name: Rust tests
run: |
set -euo pipefail
if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi
bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" test //crates/...
- name: Clippy (workspace lint policy on opted-in crates)
run: |
bazelisk query "kind('rust_library|rust_shared_library', //crates/pi-ast/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-walker/...)" \
| xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy-strict --
- name: Clippy (default lints elsewhere)
run: |
bazelisk query "kind('rust_library|rust_shared_library', //crates/... - (//crates/pi-ast/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-walker/...) - //crates/vendor/brush-core/... - //crates/vendor/brush-builtins/...)" \
| xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy --
- name: Rustfmt
run: bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=rustfmt //crates/...
- name: Build native addons
run: |
set -euo pipefail
bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build //:natives-linux-x64-baseline //:natives-linux-x64-modern
- name: Save bazel disk cache
if: steps.cache.outputs.save-needed == 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/omp-bazel-disk
key: ${{ steps.cache.outputs.cache-key }}
- uses: ./.github/actions/bun-install
+48 -129
View File
@@ -124,26 +124,23 @@ jobs:
- name: Build collab web
run: bun run collab:web:build
# Rust validation (tests, clippy, rustfmt) and native addon production are
# separate jobs: TS test shards wait only on the addons, and on
# native-changing PRs validation no longer delays artifact production.
# Rust validation (tests, clippy, rustfmt) runs on non-PR events only.
# Native-changing PRs are rare enough that they do not warrant PR-side
# Rust validation or a PR-side addon build (see native_addons): Rust is
# validated post-merge on main (kata remote cache, warm) and again at
# release. A skipped required check still satisfies branch protection.
rust_validate:
name: Validate Rust workspace (bazel)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
if: github.event_name != 'pull_request'
runs-on: omp-kata
steps:
- uses: actions/checkout@v4
- id: inputs
uses: ./.github/actions/native-inputs
# TS-only PRs skip Rust validation entirely.
- if: steps.inputs.outputs.rust == 'true'
uses: ./.github/actions/bun-install
- uses: ./.github/actions/bun-install
- id: cache
if: steps.inputs.outputs.rust == 'true'
uses: ./.github/actions/bazel-cache
with:
scope: linux
- name: Rust tests
if: steps.inputs.outputs.rust == 'true'
# The ulimit guard runs in the step that launches the bazel server
# (limits are per-process and the server persists across steps).
run: |
@@ -154,67 +151,49 @@ jobs:
# `[lints] workspace = true` get the workspace policy, the vendored
# brush fork is exempt (same as run-rs-task.ts's cargo excludes).
- name: Clippy (workspace lint policy on opted-in crates)
if: steps.inputs.outputs.rust == 'true'
run: |
bazelisk query "kind('rust_library|rust_shared_library', //crates/pi-ast/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-walker/...)" \
| xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy-strict --
- name: Clippy (default lints elsewhere)
if: steps.inputs.outputs.rust == 'true'
run: |
bazelisk query "kind('rust_library|rust_shared_library', //crates/... - (//crates/pi-ast/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-walker/...) - //crates/vendor/brush-core/... - //crates/vendor/brush-builtins/...)" \
| xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy --
- name: Rustfmt
if: steps.inputs.outputs.rust == 'true'
run: bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=rustfmt //crates/...
# No disk-cache save here: native_addons saves this same key
# concurrently, and an immutable archive can have only one producer
# per key — a validation-only archive winning the race would
# suppress the addon half on every later exact hit. Validation
# actions are seeded by the main cache warmer's combined archive.
# Builds the native addons every downstream job installs. TS-only PRs
# restore the prebuilt Linux x64 pair published by trusted main builds
# (exact content-addressed key, smoke-loaded before use) and skip Bazel
# entirely; anything else builds with bazel. Main builds all Linux-hosted
# targets, pull requests only the x64 pair tests require.
# Provides the native addons every downstream TS job installs. PRs never
# build: they fetch the latest release's Linux x64 pair from the
# @oh-my-pi/pi-natives-linux-x64 npm leaf instead. The workspace loader
# skips its version sentinel for workspace loads, so release addons load
# fine under a newer checkout; a PR whose TS tests depend on changed
# native behavior fails visibly and the native side lands via main.
# Main (kata, cluster remote cache) builds all Linux-hosted targets with
# bazel. Job name says "bazel" for continuity with required checks.
native_addons:
name: Build native addons (bazel)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
steps:
- uses: actions/checkout@v4
- id: inputs
uses: ./.github/actions/native-inputs
# Trusted fast path. Exact-key restores only — a prefix fallback
# could resolve valid-but-wrong .node files under a changed
# target/profile (the key embeds schema + os/arch + target pair +
# build profile + input hash; see native-inputs action).
- name: Restore prebuilt native addons
id: prebuilt
# ---- PR path: latest release addons from npm ----
- name: Fetch release native addons (npm)
if: github.event_name == 'pull_request'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/omp-native-addons
key: ${{ steps.inputs.outputs.cache-key }}
- name: Stage cached addons
if: steps.prebuilt.outputs.cache-hit == 'true'
shell: bash
# Canonical filenames come from the target map in
# scripts/bazel-natives.ts; staging reproduces the bazel-bin
# layout the artifact upload below globs.
run: |
set -euo pipefail
tarball="$(npm view @oh-my-pi/pi-natives-linux-x64@latest dist.tarball)"
echo "Fetching $tarball"
curl -fsSL --retry 3 "$tarball" | tar -xz -C "$RUNNER_TEMP"
mkdir -p bazel-bin/natives-linux-x64-baseline bazel-bin/natives-linux-x64-modern
cp ~/.cache/omp-native-addons/pi_natives.linux-x64-baseline.node bazel-bin/natives-linux-x64-baseline/
cp ~/.cache/omp-native-addons/pi_natives.linux-x64-modern.node bazel-bin/natives-linux-x64-modern/
# A poisoned cache entry must not ship: load both addons before
# trusting them. Failure falls back to a full build (loudly) rather
# than failing the PR.
- name: Smoke cached addons
id: smoke
if: steps.prebuilt.outputs.cache-hit == 'true'
cp "$RUNNER_TEMP/package/pi_natives.linux-x64-baseline.node" bazel-bin/natives-linux-x64-baseline/
cp "$RUNNER_TEMP/package/pi_natives.linux-x64-modern.node" bazel-bin/natives-linux-x64-modern/
# A corrupt download must fail here, not as a confusing dlopen error
# in every downstream test shard.
- name: Smoke release addons
if: github.event_name == 'pull_request'
shell: bash
run: |
set -uo pipefail
set -euo pipefail
# NOTE: `bun -e 'require("./x.node")'` swallows dlopen failures
# (exit 0 on a bogus addon); a script file enforces them in
# both bun and node. Verified against a corrupt .node fixture.
@@ -229,50 +208,40 @@ jobs:
EOF
loader=node
if command -v bun >/dev/null 2>&1; then loader=bun; fi
if "$loader" "$RUNNER_TEMP/smoke-addons.js" \
"$PWD/bazel-bin/natives-linux-x64-baseline/pi_natives.linux-x64-baseline.node" \
"$PWD/bazel-bin/natives-linux-x64-modern/pi_natives.linux-x64-modern.node"; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::cached native addons failed to load; falling back to a full bazel build"
rm -rf bazel-bin
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Decide build path
id: decide
"$loader" "$RUNNER_TEMP/smoke-addons.js" \
"$PWD/bazel-bin/natives-linux-x64-baseline/pi_natives.linux-x64-baseline.node" \
"$PWD/bazel-bin/natives-linux-x64-modern/pi_natives.linux-x64-modern.node"
# Make the policy visible on the rare native-touching PR instead of
# leaving a reviewer to wonder which addons the tests exercised.
# Best-effort: a diff-API failure must not fail the job over a notice.
- name: Note native changes tested against release addons
if: github.event_name == 'pull_request'
shell: bash
env:
HIT: ${{ steps.prebuilt.outputs.cache-hit }}
SMOKE: ${{ steps.smoke.outputs.ok }}
GH_TOKEN: ${{ github.token }}
run: |
if [ "$HIT" = "true" ] && [ "$SMOKE" = "true" ]; then
echo "Prebuilt addons restored and verified; skipping bazel."
echo "needed=false" >> "$GITHUB_OUTPUT"
else
echo "needed=true" >> "$GITHUB_OUTPUT"
if gh pr diff ${{ github.event.pull_request.number }} --name-only 2>/dev/null \
| grep -qE '^(crates/|bazel/|Cargo\.(toml|lock)|MODULE\.bazel(\.lock)?|BUILD\.bazel|\.bazelrc|\.bazelignore|\.bazelversion|rust-toolchain\.toml|rustfmt\.toml)'; then
echo "::notice title=Native sources changed::PR CI tests against the latest release addons by design; native changes are validated post-merge on main and at release."
fi
# ---- main path: bazel build of all Linux-hosted targets ----
- id: cache
if: steps.decide.outputs.needed == 'true'
if: github.event_name != 'pull_request'
uses: ./.github/actions/bazel-cache
with:
scope: linux
- name: Build native addons once
if: steps.decide.outputs.needed == 'true'
env:
EVENT_NAME: ${{ github.event_name }}
if: github.event_name != 'pull_request'
run: |
set -eo pipefail
if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi
targets=(//:natives-linux-x64-baseline //:natives-linux-x64-modern)
if [ "$EVENT_NAME" != "pull_request" ]; then
targets=(//:natives-linux-all)
fi
bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build "${targets[@]}" 2>&1 | tee "$RUNNER_TEMP/bazel-build.log"
bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build //:natives-linux-all 2>&1 | tee "$RUNNER_TEMP/bazel-build.log"
# Cache-hit visibility: a supposedly warm build that executes
# thousands of actions is the failure mode that made CI slow — make
# it visible in the run summary instead of discovering it weeks in.
- name: Report bazel cache stats
if: steps.decide.outputs.needed == 'true'
if: github.event_name != 'pull_request'
shell: bash
run: |
# Bazel runs with --color=yes (config=ci); strip ANSI before
@@ -280,57 +249,7 @@ jobs:
# the raw log.
summary=$(sed -E 's/\x1b\[[0-9;]*m//g' "$RUNNER_TEMP/bazel-build.log" | grep -E '[0-9]+ processes:' | tail -1 || true)
echo "::notice title=Bazel build summary::${summary:-no process summary found}"
- name: Save Bazel disk cache
if: steps.cache.outputs.save-needed == 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/omp-bazel-disk
key: ${{ steps.cache.outputs.cache-key }}
# Producer side of the fast path: trusted main builds publish the
# x64 pair under the content-addressed key. Smoke-load before save —
# an exact-key archive is never overwritten, so bad bytes would
# poison every TS-only PR until manual eviction.
- name: Stage built addons for cache
if: github.event_name != 'pull_request'
shell: bash
run: |
set -euo pipefail
mkdir -p ~/.cache/omp-native-addons
cp bazel-bin/natives-linux-x64-baseline/pi_natives.linux-x64-baseline.node \
bazel-bin/natives-linux-x64-modern/pi_natives.linux-x64-modern.node \
~/.cache/omp-native-addons/
- name: Smoke addons before caching
if: github.event_name != 'pull_request'
shell: bash
run: |
set -euo pipefail
cd ~/.cache/omp-native-addons
cat > "$RUNNER_TEMP/smoke-addons.js" <<'EOF'
for (const f of process.argv.slice(2)) {
const m = require(f);
if (!m || Object.keys(m).length === 0) {
console.error(`addon failed to load: ${f}`);
process.exit(1);
}
}
EOF
bun "$RUNNER_TEMP/smoke-addons.js" \
"$PWD/pi_natives.linux-x64-baseline.node" \
"$PWD/pi_natives.linux-x64-modern.node"
- name: Look up native addon cache
id: addon-cache
if: github.event_name != 'pull_request'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/omp-native-addons
key: ${{ steps.inputs.outputs.cache-key }}
lookup-only: true
- name: Save native addon cache
if: github.event_name != 'pull_request' && steps.addon-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/omp-native-addons
key: ${{ steps.inputs.outputs.cache-key }}
- name: Upload native addon artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: