feat: streamlined native addon builds and caching in ci workflows
- Enhance CI workflows to build and reuse native addon artifacts instead of recompiling. - Update bazel cache actions with selective backend detection and separate remote and disk modes. - Add `--source` CLI option to install prebuilt native targets without requiring Bazel. - Increase Kata runner memory configuration from 12Gi to 24Gi.
This commit is contained in:
@@ -1,34 +1,22 @@
|
||||
name: "Compose bazel cache config"
|
||||
description: >
|
||||
Single source of truth for how a CI job caches bazel work, emitted as a
|
||||
bazelrc fragment (rc output) consumers pass via `bazelisk --bazelrc=...`.
|
||||
Selects the CI cache backend and emits a bazelrc fragment. A shell probe
|
||||
exposes the backend through step outputs before any action condition uses it.
|
||||
|
||||
omp-kata pods (detected via BAZEL_REMOTE_USER/BAZEL_REMOTE_PASSWORD from the
|
||||
bazel-remote-ci secret) get read-write gRPC access to the in-cluster
|
||||
bazel-remote service — an address that only resolves inside the cluster, so
|
||||
nothing about the infrastructure leaks from this public repo. With
|
||||
`export: true` (the main-push rust job), the kata job additionally writes a
|
||||
bazel disk cache and saves it to the GitHub Actions cache at job end under
|
||||
the main branch scope — that is what makes pull requests warm: PR-created
|
||||
caches are never shared across PRs, main-created ones are readable by every
|
||||
PR.
|
||||
omp-kata jobs use the cluster remote cache. An exporting main job first
|
||||
performs an exact GitHub cache lookup without downloading the archive. A
|
||||
genuine miss switches that job to a local disk cache for one build. The
|
||||
workflow saves that populated cache explicitly after the build.
|
||||
|
||||
GitHub-hosted runners never talk to the cluster: they restore the
|
||||
main-exported disk cache (plus their own branch-scoped refresh saves).
|
||||
GitHub-hosted jobs restore the exported disk cache and never contact the
|
||||
cluster. Build callers can save a new exact-key archive after a miss.
|
||||
|
||||
inputs:
|
||||
scope:
|
||||
description: >
|
||||
Disk-cache key discriminator. Every linux-family consumer (validation,
|
||||
TS test jobs, linux/musl/win32 release jobs) shares the `linux` scope
|
||||
so PRs hit the cache exported from main's rust job; darwin release
|
||||
jobs keep per-target scopes and self-populate.
|
||||
description: Disk-cache key discriminator shared by compatible consumers
|
||||
required: true
|
||||
export:
|
||||
description: >
|
||||
Write a disk cache during the build and save it to the GitHub cache
|
||||
at job end (main-push rust job only). No-op when the key already
|
||||
exists for the current lockfiles.
|
||||
description: Prepare a portable disk cache after an exact lookup miss
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
@@ -36,78 +24,120 @@ outputs:
|
||||
rc:
|
||||
description: Path to the generated bazelrc fragment
|
||||
value: ${{ steps.compose.outputs.rc }}
|
||||
cache-key:
|
||||
description: Exact GitHub cache key for a later explicit save
|
||||
value: ${{ steps.backend.outputs.cache-key }}
|
||||
export-needed:
|
||||
description: Whether the remote exporter switched to a portable disk cache
|
||||
value: ${{ steps.compose.outputs.export-needed }}
|
||||
save-needed:
|
||||
description: Whether a disk-cache build can save a new exact-key archive
|
||||
value: ${{ steps.compose.outputs.save-needed }}
|
||||
remote:
|
||||
description: Whether the shell probe selected the cluster remote cache
|
||||
value: ${{ steps.backend.outputs.remote }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Restore bazel disk cache (GitHub-hosted)
|
||||
if: env.BAZEL_REMOTE_USER == ''
|
||||
uses: actions/cache@v4
|
||||
- name: Detect bazel cache backend
|
||||
id: backend
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_KEY: bazel-disk-${{ inputs.scope }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.Bazel.lock', 'MODULE.bazel', 'rust-toolchain.toml') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
remote=false
|
||||
if [ -n "${BAZEL_REMOTE_USER:-}" ] || [ -n "${BAZEL_REMOTE_PASSWORD:-}" ]; then
|
||||
if [ -z "${BAZEL_REMOTE_USER:-}" ] || [ -z "${BAZEL_REMOTE_PASSWORD:-}" ]; then
|
||||
echo "::error::BAZEL_REMOTE_USER and BAZEL_REMOTE_PASSWORD must both be set"
|
||||
exit 1
|
||||
fi
|
||||
remote=true
|
||||
fi
|
||||
{
|
||||
echo "remote=$remote"
|
||||
echo "cache-key=$CACHE_KEY"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore bazel disk cache
|
||||
id: restore
|
||||
if: steps.backend.outputs.remote != 'true'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/omp-bazel-disk
|
||||
~/.cache/omp-bazel-repo
|
||||
key: bazel-disk-${{ inputs.scope }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.Bazel.lock', 'MODULE.bazel', 'rust-toolchain.toml') }}
|
||||
path: ~/.cache/omp-bazel-disk
|
||||
key: ${{ steps.backend.outputs.cache-key }}
|
||||
restore-keys: |
|
||||
bazel-disk-${{ inputs.scope }}-${{ runner.os }}-${{ runner.arch }}-
|
||||
|
||||
# Combined restore+save: restore is a no-op on the first run for these
|
||||
# lockfiles (that's exactly when we want to build the export), and the
|
||||
# post-job save only fires on a primary-key miss — so the export is
|
||||
# written once per lockfile change, from a trusted main push.
|
||||
- name: Prepare disk cache export (omp-kata)
|
||||
if: env.BAZEL_REMOTE_USER != '' && inputs.export == 'true'
|
||||
- name: Look up bazel disk cache export
|
||||
if: steps.backend.outputs.remote == 'true' && inputs.export == 'true'
|
||||
id: export
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.cache/omp-bazel-disk
|
||||
key: bazel-disk-${{ inputs.scope }}-Linux-X64-${{ hashFiles('Cargo.Bazel.lock', 'MODULE.bazel', 'rust-toolchain.toml') }}
|
||||
key: ${{ steps.backend.outputs.cache-key }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Compose cache config
|
||||
id: compose
|
||||
shell: bash
|
||||
env:
|
||||
EXPORT_DISK: ${{ inputs.export == 'true' && steps.export.outputs.cache-hit != 'true' && 'true' || 'false' }}
|
||||
REMOTE: ${{ steps.backend.outputs.remote }}
|
||||
EXPORT_REQUESTED: ${{ inputs.export }}
|
||||
EXPORT_HIT: ${{ steps.export.outputs.cache-hit }}
|
||||
RESTORE_HIT: ${{ steps.restore.outputs.cache-hit }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export_needed=false
|
||||
if [ "$REMOTE" = "true" ] && [ "$EXPORT_REQUESTED" = "true" ] && [ "$EXPORT_HIT" != "true" ]; then
|
||||
export_needed=true
|
||||
fi
|
||||
save_needed=$export_needed
|
||||
if [ "$REMOTE" != "true" ] && [ "$RESTORE_HIT" != "true" ]; then
|
||||
save_needed=true
|
||||
fi
|
||||
|
||||
rc="$RUNNER_TEMP/bazel-cache.rc"
|
||||
if [ -n "${BAZEL_REMOTE_USER:-}" ]; then
|
||||
auth="$(printf %s "${BAZEL_REMOTE_USER}:${BAZEL_REMOTE_PASSWORD}" | base64 | tr -d '\n')"
|
||||
if [ "$REMOTE" = "true" ]; then
|
||||
{
|
||||
# The PVC repo-cache mount lives OUTSIDE $HOME (/opt): kubelet
|
||||
# creates missing mountpoint parents root-owned, and a
|
||||
# root-owned $HOME/.cache breaks bazel's default
|
||||
# output_user_root and zig's wrapper cache. Pods are
|
||||
# single-job ephemeral, so RUNNER_TEMP hosts the output root.
|
||||
# The PVC mount lives outside $HOME. Pods are single-job and
|
||||
# use RUNNER_TEMP for Bazel's output root.
|
||||
echo "startup --output_user_root=$RUNNER_TEMP/bazel-root"
|
||||
echo "common --config=ci"
|
||||
echo "common --config=cache-rw"
|
||||
echo "common --remote_cache=grpcs://bazel-remote.bazel-cache.svc.cluster.local:9092"
|
||||
echo "common --tls_certificate=infra/bazel-remote/ca.crt"
|
||||
echo "common --remote_header='authorization=Basic ${auth}'"
|
||||
# PVC-backed shared repository cache (pods are ephemeral; without
|
||||
# it every job re-downloads toolchains + crate archives). The
|
||||
# xwin MSVC splat reuses the same PVC via OMP_XWIN_CACHE_DIR
|
||||
# (its ~1GiB CDN payload is not repository-cacheable).
|
||||
echo "common --repository_cache=/opt/bazel-repo-cache"
|
||||
echo "common --repo_env=OMP_XWIN_CACHE_DIR=/opt/bazel-repo-cache/xwin"
|
||||
if [ "$EXPORT_DISK" = "true" ]; then
|
||||
# Export runs (once per lockfile change) write the disk cache
|
||||
# PRs restore. They must download everything: with top-level-
|
||||
# only downloading, remote hits would export action entries
|
||||
# whose blobs were never materialized.
|
||||
echo "common --disk_cache=$HOME/.cache/omp-bazel-disk"
|
||||
else
|
||||
# Cache-hit work stays metadata-only; only requested top-level
|
||||
# outputs (the .node addons) are actually downloaded.
|
||||
echo "common --remote_download_toplevel"
|
||||
fi
|
||||
} > "$rc"
|
||||
|
||||
if [ "$export_needed" = "true" ]; then
|
||||
# Do not combine remote and disk caches. Remote hits do not
|
||||
# materialize a portable disk cache for hosted runners.
|
||||
mkdir -p "$HOME/.cache/omp-bazel-disk"
|
||||
echo "common --disk_cache=$HOME/.cache/omp-bazel-disk" >> "$rc"
|
||||
else
|
||||
raw_auth="${BAZEL_REMOTE_USER}:${BAZEL_REMOTE_PASSWORD}"
|
||||
auth="$(printf %s "$raw_auth" | base64 | tr -d '\n')"
|
||||
echo "::add-mask::$raw_auth"
|
||||
echo "::add-mask::$auth"
|
||||
{
|
||||
echo "common --config=cache-rw"
|
||||
echo "common --remote_cache=grpcs://bazel-remote.bazel-cache.svc.cluster.local:9092"
|
||||
echo "common --tls_certificate=infra/bazel-remote/ca.crt"
|
||||
echo "common --remote_header='authorization=Basic ${auth}'"
|
||||
echo "common --remote_download_toplevel"
|
||||
} >> "$rc"
|
||||
fi
|
||||
else
|
||||
mkdir -p "$HOME/.cache/omp-bazel-disk" "$HOME/.cache/omp-bazel-repo"
|
||||
{
|
||||
echo "common --config=ci"
|
||||
echo "common --disk_cache=$HOME/.cache/omp-bazel-disk"
|
||||
echo "common --repository_cache=$HOME/.cache/omp-bazel-repo"
|
||||
} > "$rc"
|
||||
fi
|
||||
echo "rc=$rc" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "rc=$rc"
|
||||
echo "export-needed=$export_needed"
|
||||
echo "save-needed=$save_needed"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -28,10 +28,20 @@ runs:
|
||||
|
||||
- name: Build native addons
|
||||
shell: bash
|
||||
env:
|
||||
OMP_BAZEL_RC: ${{ steps.cache.outputs.rc }}
|
||||
NATIVE_TARGETS: ${{ inputs.targets }}
|
||||
NATIVE_DEST: ${{ inputs.dest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Raise-only fd guard: huge toolchain input trees exhaust low soft
|
||||
# limits during sandbox setup.
|
||||
if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi
|
||||
export OMP_BAZEL_RC="${{ steps.cache.outputs.rc }}"
|
||||
bun scripts/bazel-natives.ts ${{ inputs.targets }} --dest "${{ inputs.dest }}"
|
||||
read -r -a targets <<< "$NATIVE_TARGETS"
|
||||
bun scripts/bazel-natives.ts "${targets[@]}" --dest "$NATIVE_DEST"
|
||||
- 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 }}
|
||||
@@ -0,0 +1,38 @@
|
||||
name: "Install native addon artifacts"
|
||||
description: >
|
||||
Downloads the native addons built once by the Rust job and installs exact
|
||||
targets without invoking Bazel.
|
||||
|
||||
inputs:
|
||||
targets:
|
||||
description: Space-separated scripts/bazel-natives.ts target names
|
||||
required: true
|
||||
dest:
|
||||
description: Destination directory for the .node files
|
||||
required: false
|
||||
default: packages/natives/native
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download native addon artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: native-addons
|
||||
path: ${{ runner.temp }}/omp-native-artifacts
|
||||
|
||||
- name: Install native addon targets
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_TARGETS: ${{ inputs.targets }}
|
||||
NATIVE_DEST: ${{ inputs.dest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
read -r -a targets <<< "$NATIVE_TARGETS"
|
||||
if [ "${#targets[@]}" -eq 0 ]; then
|
||||
echo "::error::At least one native addon target is required"
|
||||
exit 1
|
||||
fi
|
||||
bun scripts/bazel-natives.ts "${targets[@]}" \
|
||||
--source "$RUNNER_TEMP/omp-native-artifacts" \
|
||||
--dest "$NATIVE_DEST"
|
||||
+52
-43
@@ -118,21 +118,16 @@ jobs:
|
||||
- name: Build collab web
|
||||
run: bun run collab:web:build
|
||||
|
||||
# Bazel validation and cache warm — replaces the old cargo pipeline
|
||||
# (rust_validation + native build matrices + hand-rolled artifact caching).
|
||||
# `bazel test` covers the Rust suite, the clippy/rustfmt aspect configs cover
|
||||
# linting, and on main pushes (omp-kata, read-write cache) an additional
|
||||
# //:natives-linux-all build populates the shared bazel-remote cache so every
|
||||
# downstream job — TS tests, releases, PR runners — gets cache hits instead
|
||||
# of rebuilding. No toolchain setup: bazelisk is on the GitHub images and
|
||||
# baked into the kata runner image; bazel fetches the rest hermetically.
|
||||
# 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:
|
||||
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 entirely; addons for the TS test
|
||||
# jobs come from the main-exported disk cache. Pushes always run.
|
||||
# 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
|
||||
@@ -153,7 +148,6 @@ jobs:
|
||||
- if: steps.changes.outputs.rust == 'true'
|
||||
uses: ./.github/actions/bun-install
|
||||
- id: cache
|
||||
if: steps.changes.outputs.rust == 'true'
|
||||
uses: ./.github/actions/bazel-cache
|
||||
with:
|
||||
scope: linux
|
||||
@@ -184,11 +178,30 @@ jobs:
|
||||
- name: Rustfmt
|
||||
if: steps.changes.outputs.rust == 'true'
|
||||
run: bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=rustfmt //crates/...
|
||||
- name: Warm native addon cache (main push)
|
||||
if: github.event_name != 'pull_request'
|
||||
- name: Build native addons once
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
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-all
|
||||
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[@]}"
|
||||
- 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 }}
|
||||
- name: Upload native addon artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: native-addons
|
||||
path: bazel-bin/natives-*/*.node
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
test_workspace:
|
||||
name: Test TS workspace fast
|
||||
@@ -200,10 +213,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test workspace packages and repo scripts (TS)
|
||||
env:
|
||||
OMP_TEST_CONCURRENCY: "4"
|
||||
@@ -219,10 +231,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test coding-agent singleton/global-state bucket
|
||||
# Keep global Settings/env/fake-timer tests serial; native addon
|
||||
# artifacts are still available like every other coding-agent bucket.
|
||||
@@ -238,10 +249,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test native/TUI/browser-ish packages (TS)
|
||||
env:
|
||||
OMP_TEST_CONCURRENCY: "4"
|
||||
@@ -257,10 +267,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test coding-agent UI/TUI bucket
|
||||
env:
|
||||
OMP_TEST_CONCURRENCY: "2"
|
||||
@@ -276,10 +285,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test coding-agent runtime bucket
|
||||
# Runtime/session tests import native-backed barrels too; keep this
|
||||
# separate for concurrency, not as a native-free guardrail.
|
||||
@@ -297,10 +305,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Test coding-agent native/unit bucket
|
||||
env:
|
||||
OMP_TEST_CONCURRENCY: "4"
|
||||
@@ -316,10 +323,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: CLI smoke test
|
||||
run: bun run ci:test:smoke
|
||||
|
||||
@@ -332,10 +338,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-system-deps
|
||||
- uses: ./.github/actions/bun-install
|
||||
- uses: ./.github/actions/bazel-natives
|
||||
- uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Install method smoke tests
|
||||
env:
|
||||
OMP_INSTALL_TEST_SKIP_NATIVE_BUILD: "1"
|
||||
@@ -398,7 +403,7 @@ jobs:
|
||||
arch: x64,
|
||||
target_id: darwin-x64,
|
||||
binary_path: packages/coding-agent/binaries/omp-darwin-x64,
|
||||
native_targets: darwin-all,
|
||||
native_targets: darwin-x64-baseline,
|
||||
}
|
||||
- {
|
||||
os: macos-14,
|
||||
@@ -406,7 +411,7 @@ jobs:
|
||||
arch: arm64,
|
||||
target_id: darwin-arm64,
|
||||
binary_path: packages/coding-agent/binaries/omp-darwin-arm64,
|
||||
native_targets: darwin-all,
|
||||
native_targets: darwin-arm64,
|
||||
}
|
||||
- {
|
||||
os: ubuntu-22.04,
|
||||
@@ -442,14 +447,20 @@ jobs:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
|
||||
- run: bun install --frozen-lockfile
|
||||
# Release runners are GitHub-hosted and never touch the private
|
||||
# cluster cache: they build with the actions/cache-backed disk cache,
|
||||
# so repeat releases with unchanged Rust are mostly local cache hits.
|
||||
- name: Build native addon(s) (bazel)
|
||||
# Linux and Windows addons come from the Rust job. Darwin runners
|
||||
# build only their own architecture because cross-hosted artifacts do
|
||||
# not exist for macOS.
|
||||
- name: Install prebuilt native addon(s)
|
||||
if: matrix.platform != 'darwin'
|
||||
uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: ${{ matrix.native_targets }}
|
||||
- name: Build native addon (bazel)
|
||||
if: matrix.platform == 'darwin'
|
||||
uses: ./.github/actions/bazel-natives
|
||||
with:
|
||||
targets: ${{ matrix.native_targets }}
|
||||
cache-scope: ${{ startsWith(matrix.target_id, 'darwin') && format('release-{0}', matrix.target_id) || 'linux' }}
|
||||
cache-scope: release-${{ matrix.target_id }}
|
||||
- name: Build release binary
|
||||
env:
|
||||
RELEASE_TARGETS: ${{ matrix.target_id }}
|
||||
@@ -609,14 +620,12 @@ jobs:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
|
||||
- run: bun install --frozen-lockfile
|
||||
# The pi-coding-agent prepack executes workspace code (bundle-dist
|
||||
# imports the pi-utils barrel, which loads the pi-natives addon), so
|
||||
# this job needs the Linux x64 native addons just like TS tests do.
|
||||
- name: Build native addons (bazel)
|
||||
uses: ./.github/actions/bazel-natives
|
||||
# The prepack executes workspace code which loads the Linux x64
|
||||
# addon, so install the Rust job's artifact before publishing.
|
||||
- name: Install prebuilt native addons
|
||||
uses: ./.github/actions/native-artifacts
|
||||
with:
|
||||
targets: linux-x64-baseline linux-x64-modern
|
||||
cache-scope: linux
|
||||
- name: Publish to npm
|
||||
env:
|
||||
# Fallback auth: setup-node wrote an .npmrc referencing
|
||||
|
||||
@@ -203,10 +203,10 @@ template:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "8"
|
||||
memory: "12Gi"
|
||||
memory: "24Gi"
|
||||
limits:
|
||||
cpu: "8"
|
||||
memory: "12Gi"
|
||||
memory: "24Gi"
|
||||
volumes:
|
||||
- name: runner-cache
|
||||
persistentVolumeClaim:
|
||||
@@ -222,7 +222,7 @@ Field by field:
|
||||
goes in a workflow's `runs-on:`.
|
||||
- **`minRunners: 0` / `maxRunners: 4`** - **scale-to-zero**. With no queued jobs
|
||||
there are zero runner microVMs. Each admitted runner gets an honest 8-vCPU,
|
||||
12-GiB request and limit; excess jobs queue instead of ten 16-vCPU guests
|
||||
24-GiB request and limit; excess jobs queue instead of ten 16-vCPU guests
|
||||
fighting over the reference host's 32 physical CPUs.
|
||||
- **`containerMode.type: ""`** - **none**. The default chart offers `dind`
|
||||
(Docker-in-Docker sidecar) or `kubernetes` mode for job-container isolation;
|
||||
@@ -255,7 +255,7 @@ Field by field:
|
||||
mounts to the `arc-runners/runner-cache` PVC. `ReadWriteOnce` is enough on this
|
||||
single-node k3s host; use a RWX-capable storage class before spreading runners
|
||||
across nodes.
|
||||
- **`resources`** - requests `2` CPU / `4Gi`, limits `16` CPU / `12Gi`. Kata reads
|
||||
- **`resources`** - requests `8` CPU / `24Gi`, limits `8` CPU / `24Gi`. Kata reads
|
||||
these and sizes the guest accordingly: the VM now boots at the same
|
||||
guaranteed floor (`default_vcpus: 2`, `default_memory: 4096`) and only
|
||||
hotplugs beyond that toward the limits, with `default_maxvcpus: 0` allowing up
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
# BUILDKIT_VERSION BuildKit release to bootstrap on demand [0.25.1]
|
||||
# RUNNER_MAX_RUNNERS maximum concurrent Kata runner pods [4]
|
||||
# RUNNER_CPU requested and limited CPU cores per runner [8]
|
||||
# RUNNER_MEMORY requested and limited memory per runner [12Gi]
|
||||
# RUNNER_MEMORY requested and limited memory per runner [24Gi]
|
||||
set -euo pipefail
|
||||
|
||||
: "${CI_HOST:?set CI_HOST to the ssh target of your CI host, e.g. CI_HOST=my-ci-host}"
|
||||
@@ -50,7 +50,7 @@ NERDCTL_VERSION="${NERDCTL_VERSION:-2.1.6}"
|
||||
BUILDKIT_VERSION="${BUILDKIT_VERSION:-0.25.1}"
|
||||
RUNNER_MAX_RUNNERS="${RUNNER_MAX_RUNNERS:-4}"
|
||||
RUNNER_CPU="${RUNNER_CPU:-8}"
|
||||
RUNNER_MEMORY="${RUNNER_MEMORY:-12Gi}"
|
||||
RUNNER_MEMORY="${RUNNER_MEMORY:-24Gi}"
|
||||
|
||||
arg="${1:-$(date +%Y-%m-%d-%H%M%S)}"
|
||||
case "$arg" in *:*) IMAGE="$arg";; *) IMAGE="omp-kata-runner:$arg";; esac
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- CI now exports Bazel disk caches only after exact misses and reuses one native addon artifact across Linux test and release jobs. macOS release jobs now build only their own architecture.
|
||||
- Native addons now build with Bazel (rules_rust + hermetic zig cc toolchains for linux-gnu/musl, host Xcode for darwin, and a hermetic clang-cl + xwin toolchain for windows-msvc) instead of the napi CLI + cargo-zigbuild/cargo-xwin pipeline. `bun run build` drives `scripts/bazel-natives.ts`; TypeScript binding regeneration moved to `bun run build:bindings` (needed only when the Rust API surface changes). CI caches through a content-addressed bazel-remote action cache instead of sccache + target-directory snapshots, cutting warm native rebuilds from ~20 minutes to seconds and cold cache-hit builds to ~2.5 minutes.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import {
|
||||
conventionOutputPaths,
|
||||
type HostInfo,
|
||||
@@ -95,21 +99,58 @@ describe("parseBazelFilesOutput", () => {
|
||||
});
|
||||
|
||||
describe("parseCliArgs", () => {
|
||||
test("splits targets, --dest, and passthrough bazel args", () => {
|
||||
test("splits targets, paths, and passthrough bazel args", () => {
|
||||
expect(
|
||||
parseCliArgs(["linux-x64-baseline", "linux-x64-modern", "--dest", "out", "--", "--config=ci", "--dest"]),
|
||||
).toEqual({
|
||||
targets: ["linux-x64-baseline", "linux-x64-modern"],
|
||||
dest: "out",
|
||||
source: null,
|
||||
bazelArgs: ["--config=ci", "--dest"],
|
||||
});
|
||||
expect(parseCliArgs(["host"])).toEqual({ targets: ["host"], dest: null, bazelArgs: [] });
|
||||
expect(parseCliArgs(["host", "--source", "artifact"])).toEqual({
|
||||
targets: ["host"],
|
||||
dest: null,
|
||||
source: "artifact",
|
||||
bazelArgs: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects missing targets, stray flags, and a valueless --dest", () => {
|
||||
test("rejects invalid build and artifact source combinations", () => {
|
||||
expect(() => parseCliArgs([])).toThrow(/Usage:/);
|
||||
expect(() => parseCliArgs(["--", "--config=ci"])).toThrow(/Usage:/);
|
||||
expect(() => parseCliArgs(["host", "--config=ci"])).toThrow(/Unknown flag --config=ci/);
|
||||
expect(() => parseCliArgs(["host", "--dest"])).toThrow(/--dest requires/);
|
||||
expect(() => parseCliArgs(["host", "--source"])).toThrow(/--source requires/);
|
||||
expect(() => parseCliArgs(["host", "--source", "artifact", "--", "--config=ci"])).toThrow(
|
||||
/--source cannot be combined/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("artifact source install", () => {
|
||||
test("installs exact target outputs without invoking Bazel", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-native-artifacts-"));
|
||||
const source = path.join(root, "source");
|
||||
const dest = path.join(root, "dest");
|
||||
const baseline = "pi_natives.linux-x64-baseline.node";
|
||||
const modern = "pi_natives.linux-x64-modern.node";
|
||||
try {
|
||||
await fs.mkdir(path.join(source, "natives-linux-x64-baseline"), { recursive: true });
|
||||
await fs.mkdir(path.join(source, "natives-linux-x64-modern"), { recursive: true });
|
||||
await Bun.write(path.join(source, "natives-linux-x64-baseline", baseline), "baseline");
|
||||
await Bun.write(path.join(source, "natives-linux-x64-modern", modern), "modern");
|
||||
|
||||
const result =
|
||||
await $`${process.execPath} ${path.join(import.meta.dir, "bazel-natives.ts")} linux-x64-baseline linux-x64-modern --source ${source} --dest ${dest}`
|
||||
.quiet()
|
||||
.nothrow();
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(await Bun.file(path.join(dest, baseline)).text()).toBe("baseline");
|
||||
expect(await Bun.file(path.join(dest, modern)).text()).toBe("modern");
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+58
-37
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Canonical Bazel driver for the shipping pi_natives addons.
|
||||
*
|
||||
* Usage: bun scripts/bazel-natives.ts <target>... [--dest <dir>] [-- <extra bazel args>]
|
||||
* Usage: bun scripts/bazel-natives.ts <target>... [--dest <dir>] [--source <dir>] [-- <extra bazel args>]
|
||||
*
|
||||
* Targets are the //:natives-* names from BUILD.bazel (e.g. linux-x64-baseline,
|
||||
* darwin-arm64) plus three pseudo-targets:
|
||||
@@ -118,15 +118,19 @@ export function parseBazelFilesOutput(output: string): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
/** Parsed options for the native addon build and artifact install modes. */
|
||||
export interface CliOptions {
|
||||
targets: string[];
|
||||
dest: string | null;
|
||||
source: string | null;
|
||||
bazelArgs: string[];
|
||||
}
|
||||
|
||||
/** Parse target names and the mutually exclusive build or artifact source options. */
|
||||
export function parseCliArgs(argv: string[]): CliOptions {
|
||||
const targets: string[] = [];
|
||||
let dest: string | null = null;
|
||||
let source: string | null = null;
|
||||
const bazelArgs: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
@@ -134,10 +138,14 @@ export function parseCliArgs(argv: string[]): CliOptions {
|
||||
bazelArgs.push(...argv.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
if (arg === "--dest") {
|
||||
if (arg === "--dest" || arg === "--source") {
|
||||
const value = argv[++i];
|
||||
if (!value) throw new Error("--dest requires a directory argument");
|
||||
dest = value;
|
||||
if (!value) throw new Error(`${arg} requires a directory argument`);
|
||||
if (arg === "--dest") {
|
||||
dest = value;
|
||||
} else {
|
||||
source = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
@@ -146,9 +154,14 @@ export function parseCliArgs(argv: string[]): CliOptions {
|
||||
targets.push(arg);
|
||||
}
|
||||
if (targets.length === 0) {
|
||||
throw new Error("Usage: bun scripts/bazel-natives.ts <target>... [--dest <dir>] [-- <extra bazel args>]");
|
||||
throw new Error(
|
||||
"Usage: bun scripts/bazel-natives.ts <target>... [--dest <dir>] [--source <dir>] [-- <extra bazel args>]",
|
||||
);
|
||||
}
|
||||
return { targets, dest, bazelArgs };
|
||||
if (source && bazelArgs.length > 0) {
|
||||
throw new Error("--source cannot be combined with extra bazel arguments");
|
||||
}
|
||||
return { targets, dest, source, bazelArgs };
|
||||
}
|
||||
|
||||
function resolveBazelBinary(): string {
|
||||
@@ -204,40 +217,48 @@ async function installAddon(sourcePath: string, destPath: string): Promise<void>
|
||||
async function main(): Promise<void> {
|
||||
const options = parseCliArgs(process.argv.slice(2));
|
||||
const host: HostInfo = { platform: process.platform, arch: process.arch, avx2: detectHostAvx2Support() };
|
||||
const labels = resolveTargetLabels(options.targets, host);
|
||||
const destDir = options.dest ? path.resolve(options.dest) : path.join(repoRoot, "packages/natives/native");
|
||||
const bazel = resolveBazelBinary();
|
||||
// CI hands cache wiring (remote or disk) through a bazelrc fragment so
|
||||
// endpoint composition stays in .github/actions/bazel-cache.
|
||||
const rcPath = Bun.env.OMP_BAZEL_RC?.trim();
|
||||
const startupArgs = rcPath ? [`--bazelrc=${rcPath}`] : [];
|
||||
|
||||
const buildArgs = [...startupArgs, "build", ...options.bazelArgs, "--", ...labels];
|
||||
console.log(`$ ${path.basename(bazel)} ${buildArgs.join(" ")}`);
|
||||
const build = await runBazel(bazel, buildArgs, "inherit");
|
||||
if (build.exitCode !== 0) {
|
||||
console.error(`\nbazel build failed (exit ${build.exitCode}). stderr tail:\n${build.stderrTail}`);
|
||||
process.exit(build.exitCode || 1);
|
||||
}
|
||||
|
||||
// Same flags as the build so cquery resolves the identical configuration.
|
||||
// cquery takes exactly one query expression, so multiple targets join
|
||||
// into a single union rather than positional args.
|
||||
const cquery = await runBazel(
|
||||
bazel,
|
||||
[...startupArgs, "cquery", ...options.bazelArgs, "--output=files", labels.join(" + ")],
|
||||
"pipe",
|
||||
);
|
||||
let outputs: string[];
|
||||
if (cquery.exitCode === 0) {
|
||||
outputs = parseBazelFilesOutput(cquery.stdout);
|
||||
|
||||
if (options.source) {
|
||||
const sourceDir = path.resolve(options.source);
|
||||
outputs = conventionOutputPaths(options.targets, host).map(output =>
|
||||
path.join(sourceDir, path.relative("bazel-bin", output)),
|
||||
);
|
||||
} else {
|
||||
console.warn(`bazel cquery failed (exit ${cquery.exitCode}); falling back to bazel-bin path convention`);
|
||||
outputs = conventionOutputPaths(options.targets, host);
|
||||
}
|
||||
if (outputs.length === 0) {
|
||||
console.error("bazel build succeeded but no .node outputs were located");
|
||||
process.exit(1);
|
||||
const labels = resolveTargetLabels(options.targets, host);
|
||||
const bazel = resolveBazelBinary();
|
||||
// CI hands cache wiring (remote or disk) through a bazelrc fragment so
|
||||
// endpoint composition stays in .github/actions/bazel-cache.
|
||||
const rcPath = Bun.env.OMP_BAZEL_RC?.trim();
|
||||
const startupArgs = rcPath ? [`--bazelrc=${rcPath}`] : [];
|
||||
|
||||
const buildArgs = [...startupArgs, "build", ...options.bazelArgs, "--", ...labels];
|
||||
console.log(`$ ${path.basename(bazel)} ${buildArgs.join(" ")}`);
|
||||
const build = await runBazel(bazel, buildArgs, "inherit");
|
||||
if (build.exitCode !== 0) {
|
||||
console.error(`\nbazel build failed (exit ${build.exitCode}). stderr tail:\n${build.stderrTail}`);
|
||||
process.exit(build.exitCode || 1);
|
||||
}
|
||||
|
||||
// Same flags as the build so cquery resolves the identical configuration.
|
||||
// cquery takes exactly one query expression, so multiple targets join
|
||||
// into a single union rather than positional args.
|
||||
const cquery = await runBazel(
|
||||
bazel,
|
||||
[...startupArgs, "cquery", ...options.bazelArgs, "--output=files", labels.join(" + ")],
|
||||
"pipe",
|
||||
);
|
||||
if (cquery.exitCode === 0) {
|
||||
outputs = parseBazelFilesOutput(cquery.stdout);
|
||||
} else {
|
||||
console.warn(`bazel cquery failed (exit ${cquery.exitCode}); falling back to bazel-bin path convention`);
|
||||
outputs = conventionOutputPaths(options.targets, host);
|
||||
}
|
||||
if (outputs.length === 0) {
|
||||
console.error("bazel build succeeded but no .node outputs were located");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Map<string, string>();
|
||||
|
||||
Reference in New Issue
Block a user