ci: restructured workflow pipelines and introduced bazel cache actions

- Updated bazel cache key generation with v2 schema version and streamlined remote cache usage.
- Added native-inputs composite action to centralize change detection and artifact caching.
- Added a scheduled workflow to warm the hosted bazel disk cache.
- Split the rust validation job and integrated prebuilt native addon caching into the CI pipeline.
This commit is contained in:
can1357
2026-07-28 11:55:57 +02:00
parent a10fe079ce
commit 0820085890
4 changed files with 308 additions and 103 deletions
+43
View File
@@ -0,0 +1,43 @@
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. A warm run restores the exact-key archive, builds
# incrementally, and saves nothing; a lockfile/config change misses, rebuilds,
# and saves the new key.
on:
schedule:
- cron: "23 */6 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: bazel-cache-warm
cancel-in-progress: true
jobs:
warm:
name: Seed hosted bazel disk cache
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- id: cache
uses: ./.github/actions/bazel-cache
with:
scope: linux
- name: Build native addons
run: |
set -euo pipefail
if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi
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 }}
+176 -53
View File
@@ -118,44 +118,26 @@ jobs:
- name: Build collab web
run: bun run collab:web:build
# One Bazel job validates Rust changes and builds native addons for every
# downstream job. Main builds all Linux-hosted targets. Pull requests build
# only the Linux x64 pair required by tests.
rust:
# 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_validate:
name: Validate Rust workspace (bazel)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
steps:
- uses: actions/checkout@v4
# TS-only PRs skip Rust validation. They still materialize the Linux
# x64 addons once from the main-exported disk cache.
- name: Detect Rust-affecting changes
id: changes
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "rust=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if gh pr diff ${{ github.event.pull_request.number }} --name-only \
| grep -qE '^(crates/|Cargo\.(toml|lock)|Cargo\.Bazel\.lock|MODULE\.bazel|BUILD\.bazel|\.bazelrc|\.bazelversion|bazel/|rust-toolchain\.toml|rustfmt\.toml|scripts/bazel-natives|\.github/actions/bazel-|\.github/workflows/ci\.yml)'; then
echo "rust=true" >> "$GITHUB_OUTPUT"
else
echo "No Rust-affecting changes; skipping validation."
echo "rust=false" >> "$GITHUB_OUTPUT"
fi
- if: steps.changes.outputs.rust == 'true'
- 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
- id: cache
if: steps.inputs.outputs.rust == 'true'
uses: ./.github/actions/bazel-cache
with:
scope: linux
# Main pushes export the disk cache PRs restore (once per
# lockfile change; no-op otherwise).
export: ${{ github.event_name != 'pull_request' }}
- name: Rust tests
if: steps.changes.outputs.rust == 'true'
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: |
@@ -166,35 +148,175 @@ 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.changes.outputs.rust == 'true'
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.changes.outputs.rust == 'true'
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.changes.outputs.rust == 'true'
if: steps.inputs.outputs.rust == 'true'
run: bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=rustfmt //crates/...
# 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.
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
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
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'
shell: bash
run: |
set -uo 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.
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
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
shell: bash
env:
HIT: ${{ steps.prebuilt.outputs.cache-hit }}
SMOKE: ${{ steps.smoke.outputs.ok }}
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"
fi
- id: cache
if: steps.decide.outputs.needed == 'true'
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 }}
run: |
set -euo pipefail
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[@]}"
bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build "${targets[@]}" 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'
shell: bash
run: |
summary=$(grep -E "INFO: [0-9]+ processes:" "$RUNNER_TEMP/bazel-build.log" | 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:
@@ -206,8 +328,8 @@ jobs:
test_workspace:
name: Test TS workspace fast
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -224,8 +346,8 @@ jobs:
test_coding_agent_singleton:
name: Test coding-agent singleton/global-state (TS)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -242,8 +364,8 @@ jobs:
test_ts_native:
name: Test TS native/integration packages
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
@@ -260,8 +382,8 @@ jobs:
test_coding_agent_ui:
name: Test coding-agent UI/TUI (TS)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
@@ -278,8 +400,8 @@ jobs:
test_coding_agent_runtime:
name: Test coding-agent runtime/session (TS)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
@@ -298,8 +420,8 @@ jobs:
test_coding_agent_native:
name: Test coding-agent native/unit (TS)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
@@ -316,8 +438,8 @@ jobs:
test_smoke:
name: Test CLI smoke (TS)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
@@ -332,8 +454,8 @@ jobs:
install_methods:
name: Install method smoke tests
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }}
needs: [rust]
if: ${{ !cancelled() && needs.rust.result == 'success' }}
needs: [native_addons]
if: ${{ !cancelled() && needs.native_addons.result == 'success' }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-system-deps
@@ -349,7 +471,8 @@ jobs:
release_binary:
name: "Release binary: ${{ matrix.target_id }}"
if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() &&
needs.rust.result == 'success' &&
needs.rust_validate.result == 'success' &&
needs.native_addons.result == 'success' &&
needs.test_workspace.result == 'success' &&
needs.test_coding_agent_singleton.result == 'success' &&
needs.test_ts_native.result == 'success' &&
@@ -358,7 +481,7 @@ jobs:
needs.test_coding_agent_native.result == 'success' &&
needs.test_smoke.result == 'success' && needs.check.result == 'success' &&
needs.install_methods.result == 'success' }}
needs: [release_metadata, check, rust, test_workspace, test_coding_agent_singleton, test_ts_native, test_coding_agent_ui, test_coding_agent_runtime, test_coding_agent_native, test_smoke, install_methods]
needs: [release_metadata, check, rust_validate, native_addons, test_workspace, test_coding_agent_singleton, test_ts_native, test_coding_agent_ui, test_coding_agent_runtime, test_coding_agent_native, test_smoke, install_methods]
strategy:
fail-fast: false
matrix:
@@ -447,7 +570,7 @@ jobs:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- run: bun install --frozen-lockfile
# Linux and Windows addons come from the Rust job. Darwin runners
# Linux and Windows addons come from the native_addons job. Darwin runners
# build only their own architecture because cross-hosted artifacts do
# not exist for macOS.
- name: Install prebuilt native addon(s)
@@ -621,7 +744,7 @@ jobs:
key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- run: bun install --frozen-lockfile
# The prepack executes workspace code which loads the Linux x64
# addon, so install the Rust job's artifact before publishing.
# addon, so install the native_addons job's artifact before publishing.
- name: Install prebuilt native addons
uses: ./.github/actions/native-artifacts
with: