@@ -315,11 +315,109 @@ jobs:
315315 use_pypi : true
316316 version : " 11.2.8"
317317
318+ cmu-nav-natives-marker :
319+ # Check whether the modules-for-this-config marker cache
320+ # already exists. Used to skip cmu-nav-natives without using the cachix environment.
321+ if : |
322+ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
323+ runs-on : ubuntu-latest
324+ permissions :
325+ contents : read # For checkout
326+ outputs :
327+ cache-hit : ${{ steps.marker.outputs.cache-hit }}
328+ steps :
329+ - name : Checkout files used for cache key
330+ uses : actions/checkout@v6
331+ with :
332+ sparse-checkout : |
333+ bin/build-cmu-nav-natives
334+ dimos/navigation/cmu_nav/modules
335+ sparse-checkout-cone-mode : false
336+ - name : Look up marker cache
337+ id : marker
338+ uses : actions/cache/restore@v4
339+ with :
340+ path : /tmp/cmu-nav-natives-marker
341+ key : cmu-nav-natives-${{ hashFiles('bin/build-cmu-nav-natives', 'dimos/navigation/cmu_nav/modules/**') }}
342+ lookup-only : true
343+
344+ cmu-nav-natives :
345+ # ⚠️ MUST NEVER RUN ON EXTERNAL PRs ⚠️
346+ # This job has write access to CACHIX_AUTH_TOKEN. Any workflow change that
347+ # reaches a `run:` step here could exfiltrate the token, letting an attacker
348+ # push poisoned binaries to the Cachix cache — which subsequent CI runs and
349+ # every dev with the substituter configured would unwittingly install. The
350+ # `if:` guard below is the only thing keeping fork PRs out — DO NOT REMOVE.
351+ if : |
352+ !cancelled() &&
353+ (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
354+ needs.cmu-nav-natives-marker.outputs.cache-hit != 'true'
355+ needs : cmu-nav-natives-marker
356+ timeout-minutes : 300
357+ environment : cachix
358+ runs-on : ubuntu-latest
359+ permissions :
360+ contents : read # For checkout
361+
362+ steps :
363+ - name : Checkout
364+ uses : actions/checkout@v6
365+ - name : Free runner disk space
366+ uses : BRAINSia/free-disk-space@v2.1.3
367+ - name : Install uv
368+ uses : astral-sh/setup-uv@v6
369+ with :
370+ enable-cache : true
371+ - name : Install minimal dimos for module discovery
372+ run : uv sync --no-default-groups --frozen
373+ - name : Install Nix (with Cachix substituter)
374+ env :
375+ INPUT_EXTRA_NIX_CONFIG : |
376+ extra-substituters = https://dimensionalos.cachix.org
377+ extra-trusted-public-keys = dimensionalos.cachix.org-1:20ynj6TjpoD3qTxkdNoeHtgs2G2pNvgAq1EQYLTHJXI=
378+ INPUT_SET_AS_TRUSTED_USER : " true"
379+ run : bash docker/ros/install-nix.sh
380+ - name : Cache Nix fetcher + eval cache
381+ # ~/.cache/nix holds the flake fetcher cache (the github tarballs of
382+ # each `nix build github:…` ref) and the eval cache (compiled flake
383+ # outputs). ~/.local/state/nix holds profile/gcroot bookkeeping.
384+ # Restoring these skips the ~20s/module evaluation cost, which is
385+ # paid every cold run otherwise. /nix/store is intentionally NOT
386+ # cached here — that's the slow heavy part that Cachix handles.
387+ uses : actions/cache@v4
388+ with :
389+ path : |
390+ ~/.cache/nix
391+ ~/.local/state/nix
392+ key : nix-fetcher-build-${{ runner.os }}-${{ hashFiles('bin/build-cmu-nav-natives', 'dimos/navigation/cmu_nav/modules/**/*.py') }}
393+ restore-keys : nix-fetcher-build-${{ runner.os }}-
394+ - name : Authenticate Cachix
395+ uses : cachix/cachix-action@v15
396+ with :
397+ name : dimensionalos
398+ authToken : ${{ secrets.CACHIX_AUTH_TOKEN }}
399+ - name : Build cmu_nav native modules
400+ env :
401+ BUILD_WORKERS : " 2"
402+ run : uv run bin/build-cmu-nav-natives
403+ - name : Write marker file
404+ run : install -D /dev/null /tmp/cmu-nav-natives-marker/ok
405+ - name : Save marker cache
406+ # Done as a final step (rather than via actions/cache's post-step) so
407+ # a build failure doesn't poison the cache with a marker for a config
408+ # we never actually pushed.
409+ uses : actions/cache/save@v4
410+ with :
411+ path : /tmp/cmu-nav-natives-marker
412+ key : cmu-nav-natives-${{ hashFiles('bin/build-cmu-nav-natives', 'dimos/navigation/cmu_nav/modules/**') }}
413+
318414 self-hosted-tests :
319415 # Skip on PRs from forks which would expose the self-hosted runner to untrusted code from external contributors.
320416 if : |
321- github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
322- needs : compute-ros-pin
417+ !cancelled() &&
418+ (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
419+ contains(fromJSON('["success", "skipped"]'), needs.cmu-nav-natives.result)
420+ needs : [compute-ros-pin, cmu-nav-natives]
323421 env :
324422 OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
325423 ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
@@ -333,6 +431,11 @@ jobs:
333431 # GitHub Actions only honours `container:` on Linux runners.
334432 container :
335433 image : ghcr.io/dimensionalos/ros-dev@${{ needs.compute-ros-pin.outputs.digest }}
434+ # Persist Nix state on the self-hosted runner's filesystem so
435+ # the ~3 GB module closures survive between jobs.
436+ volumes :
437+ - /var/cache/dimos-nix:/nix
438+ - /var/cache/dimos-root-cache:/root/.cache
336439 markers : " self_hosted or skipif_no_ros"
337440 experimental : false
338441 - os : macOS
@@ -396,6 +499,35 @@ jobs:
396499 echo "ROS_VERSION=$ROS_VERSION"
397500 echo "ROS_PYTHON_VERSION=$ROS_PYTHON_VERSION"
398501 } >> "$GITHUB_ENV"
502+ - name : Install Nix (with Cachix substituter)
503+ if : contains(matrix.markers, 'skipif_no_ros')
504+ # /nix is bind-mounted from the self-hosted runner's host filesystem
505+ # (see this job's matrix container.volumes). On warm runs Nix is
506+ # already installed; install-nix.sh's `type -p nix` check will exit
507+ # early, but only if the profile bin is on PATH — prepend it before
508+ # calling. If /nix is empty (first job on a fresh runner), nix isn't
509+ # on PATH and the install proceeds normally. After install-nix.sh,
510+ # we unconditionally (re-)write /etc/nix/nix.conf: on warm runs
511+ # install-nix.sh exited before it would have written the config,
512+ # which leaves `experimental-features = nix-command flakes` absent
513+ # and the build dying with "experimental Nix feature 'nix-command'
514+ # is disabled". Writing it here keeps cold and warm runs identical.
515+ run : |
516+ export PATH="/nix/var/nix/profiles/default/bin:$PATH"
517+ bash docker/ros/install-nix.sh
518+ sudo mkdir -p /etc/nix
519+ sudo tee /etc/nix/nix.conf > /dev/null <<'EOF'
520+ experimental-features = nix-command flakes
521+ extra-substituters = https://dimensionalos.cachix.org
522+ extra-trusted-public-keys = dimensionalos.cachix.org-1:20ynj6TjpoD3qTxkdNoeHtgs2G2pNvgAq1EQYLTHJXI=
523+ always-allow-substitutes = true
524+ build-users-group =
525+ max-jobs = auto
526+ EOF
527+ echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
528+ - name : Fetch cmu_nav native modules from Cachix
529+ if : contains(matrix.markers, 'skipif_no_ros')
530+ run : uv run bin/build-cmu-nav-natives
399531 - name : Run tests
400532 run : uv run pytest --cov=dimos/ --junitxml=junit.xml -m '(${{ matrix.markers }}) and not mujoco'
401533 - name : Re-run the failing tests with maximum verbosity
@@ -559,6 +691,7 @@ jobs:
559691 - md-babel
560692 - docs-validate
561693 - tests
694+ - cmu-nav-natives
562695 - self-hosted-tests
563696 # - self-hosted-large-tests
564697
@@ -570,9 +703,7 @@ jobs:
570703 - name : Decide whether the needed jobs succeeded or failed
571704 uses : re-actors/alls-green@release/v1
572705 with :
573- allowed-skips : |
574- self-hosted-tests
575- self-hosted-large-tests
706+ allowed-skips : cmu-nav-natives, self-hosted-tests, self-hosted-large-tests
576707 jobs : ${{ toJSON(needs) }}
577708 - name : Trigger Codecov notifications
578709 uses : codecov/codecov-action@v6
0 commit comments