From a691c74207370999f56351bd95e2a65c2fe3a07b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 15 Feb 2026 21:02:16 -0500 Subject: [PATCH 1/3] Nix: speed up gateway builds and add cache-only safeguards --- .github/workflows/cache-only.yml | 80 +++++++++++++++++++++- .github/workflows/config-options-check.yml | 36 +++++++++- .github/workflows/hm-activation-linux.yml | 36 +++++++++- flake.nix | 3 + nix/checks/openclaw-gateway-smoke.nix | 22 ++++++ nix/packages/openclaw-gateway.nix | 1 + nix/scripts/check-gateway-smoke.sh | 11 +++ nix/scripts/gateway-install.sh | 22 +++++- 8 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 nix/checks/openclaw-gateway-smoke.nix create mode 100755 nix/scripts/check-gateway-smoke.sh diff --git a/.github/workflows/cache-only.yml b/.github/workflows/cache-only.yml index 452209ce..59cdd457 100644 --- a/.github/workflows/cache-only.yml +++ b/.github/workflows/cache-only.yml @@ -1,4 +1,4 @@ -name: Cache Only +name: Cache Pipeline on: pull_request: @@ -9,8 +9,8 @@ on: types: [ completed ] jobs: - cache-only: - if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} + producer-cache-ready: + if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} runs-on: ubuntu-latest env: TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} @@ -68,9 +68,12 @@ jobs: packages.x86_64-linux.openclaw-gateway packages.x86_64-linux.openclaw-tools checks.aarch64-darwin.gateway + checks.aarch64-darwin.gateway-smoke checks.x86_64-linux.gateway + checks.x86_64-linux.gateway-smoke checks.x86_64-linux.gateway-tests checks.x86_64-linux.config-options + checks.x86_64-linux.hm-activation ) deadline=$(( $(date +%s) + WAIT_MINUTES * 60 )) @@ -98,3 +101,74 @@ jobs: echo "Cache still missing (${#missing[@]}). Retrying in 30s..." sleep 30 done + + consumer-cache-only: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} + runs-on: ubuntu-latest + env: + TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ env.TARGET_SHA }} + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v13 + + - name: Wait for Garnix checks + uses: actions/github-script@v7 + with: + script: | + const waitMinutes = 30 + const intervalMs = 30_000 + const deadline = Date.now() + waitMinutes * 60 * 1000 + const targetSha = process.env.TARGET_SHA || context.sha + + while (true) { + const { data } = await github.rest.checks.listForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: targetSha, + }) + const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') + if (garnix && garnix.status === 'completed') { + if (garnix.conclusion !== 'success') { + core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) + } + break + } + if (Date.now() > deadline) { + core.setFailed('Timed out waiting for Garnix checks') + break + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } + + - name: Consume Linux checks from cache only + run: | + nix build --extra-experimental-features 'nix-command flakes' \ + --accept-flake-config \ + --max-jobs 0 \ + .#checks.x86_64-linux.ci \ + --print-build-logs + + - name: Consume Darwin artifacts from cache only + env: + STORE_URL: https://cache.garnix.io + run: | + set -euo pipefail + + targets=( + packages.aarch64-darwin.openclaw + packages.aarch64-darwin.openclaw-gateway + packages.aarch64-darwin.openclaw-tools + packages.aarch64-darwin.openclaw-app + checks.aarch64-darwin.gateway + checks.aarch64-darwin.gateway-smoke + ) + + for target in "${targets[@]}"; do + out_path=$(nix --extra-experimental-features 'nix-command flakes' eval --accept-flake-config --raw ".#${target}.outPath") + nix path-info --store "$STORE_URL" "$out_path" >/dev/null + done diff --git a/.github/workflows/config-options-check.yml b/.github/workflows/config-options-check.yml index d479fb47..13f95a7d 100644 --- a/.github/workflows/config-options-check.yml +++ b/.github/workflows/config-options-check.yml @@ -26,5 +26,39 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@v13 + - name: Wait for Garnix checks + uses: actions/github-script@v7 + with: + script: | + const waitMinutes = 30 + const intervalMs = 30_000 + const deadline = Date.now() + waitMinutes * 60 * 1000 + const targetSha = process.env.TARGET_SHA || context.sha + + while (true) { + const { data } = await github.rest.checks.listForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: targetSha, + }) + const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') + if (garnix && garnix.status === 'completed') { + if (garnix.conclusion !== 'success') { + core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) + } + break + } + if (Date.now() > deadline) { + core.setFailed('Timed out waiting for Garnix checks') + break + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } + - name: Verify config options are up to date - run: nix build .#checks.x86_64-linux.config-options --print-build-logs + run: | + nix build --extra-experimental-features 'nix-command flakes' \ + --accept-flake-config \ + --max-jobs 0 \ + .#checks.x86_64-linux.config-options \ + --print-build-logs diff --git a/.github/workflows/hm-activation-linux.yml b/.github/workflows/hm-activation-linux.yml index 9b657f2c..7bfc0518 100644 --- a/.github/workflows/hm-activation-linux.yml +++ b/.github/workflows/hm-activation-linux.yml @@ -23,5 +23,39 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@v13 + - name: Wait for Garnix checks + uses: actions/github-script@v7 + with: + script: | + const waitMinutes = 30 + const intervalMs = 30_000 + const deadline = Date.now() + waitMinutes * 60 * 1000 + const targetSha = process.env.TARGET_SHA || context.sha + + while (true) { + const { data } = await github.rest.checks.listForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: targetSha, + }) + const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') + if (garnix && garnix.status === 'completed') { + if (garnix.conclusion !== 'success') { + core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) + } + break + } + if (Date.now() > deadline) { + core.setFailed('Timed out waiting for Garnix checks') + break + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } + - name: Run HM activation - run: nix build .#checks.x86_64-linux.hm-activation --print-build-logs + run: | + nix build --extra-experimental-features 'nix-command flakes' \ + --accept-flake-config \ + --max-jobs 0 \ + .#checks.x86_64-linux.hm-activation \ + --print-build-logs diff --git a/flake.nix b/flake.nix index 6238ebac..39c4f913 100644 --- a/flake.nix +++ b/flake.nix @@ -69,6 +69,9 @@ let baseChecks = { gateway = packageSetStable.openclaw-gateway; + gateway-smoke = pkgs.callPackage ./nix/checks/openclaw-gateway-smoke.nix { + openclawGateway = packageSetStable.openclaw-gateway; + }; package-contents = pkgs.callPackage ./nix/checks/openclaw-package-contents.nix { openclawGateway = packageSetStable.openclaw-gateway; }; diff --git a/nix/checks/openclaw-gateway-smoke.nix b/nix/checks/openclaw-gateway-smoke.nix new file mode 100644 index 00000000..2d6944fe --- /dev/null +++ b/nix/checks/openclaw-gateway-smoke.nix @@ -0,0 +1,22 @@ +{ + lib, + stdenv, + openclawGateway, +}: + +stdenv.mkDerivation { + pname = "openclaw-gateway-smoke"; + version = lib.getVersion openclawGateway; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + env = { + OPENCLAW_GATEWAY = openclawGateway; + }; + + doCheck = true; + checkPhase = "${../scripts/check-gateway-smoke.sh}"; + installPhase = "${../scripts/empty-install.sh}"; +} diff --git a/nix/packages/openclaw-gateway.nix b/nix/packages/openclaw-gateway.nix index 58b6edc9..bddc649a 100644 --- a/nix/packages/openclaw-gateway.nix +++ b/nix/packages/openclaw-gateway.nix @@ -83,6 +83,7 @@ stdenv.mkDerivation (finalAttrs: { postPatch = "${../scripts/gateway-postpatch.sh}"; buildPhase = "${../scripts/gateway-build.sh}"; installPhase = "${../scripts/gateway-install.sh}"; + dontFixup = true; dontStrip = true; dontPatchShebangs = true; diff --git a/nix/scripts/check-gateway-smoke.sh b/nix/scripts/check-gateway-smoke.sh new file mode 100755 index 00000000..aa38254d --- /dev/null +++ b/nix/scripts/check-gateway-smoke.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +if [ -z "${OPENCLAW_GATEWAY:-}" ]; then + echo "OPENCLAW_GATEWAY is not set" >&2 + exit 1 +fi + +export HOME="$(mktemp -d)" + +"$OPENCLAW_GATEWAY/bin/openclaw" --help >/dev/null diff --git a/nix/scripts/gateway-install.sh b/nix/scripts/gateway-install.sh index 3fce97d3..b7f239be 100755 --- a/nix/scripts/gateway-install.sh +++ b/nix/scripts/gateway-install.sh @@ -17,9 +17,27 @@ log_step() { printf '>> [timing] %s: %ss\n' "$name" "$((end - start))" >&2 } +check_no_broken_symlinks() { + root="$1" + if [ ! -d "$root" ]; then + return 0 + fi + + broken_tmp="$(mktemp)" + find "$root" -type l ! -exec test -e {} \; -print > "$broken_tmp" + if [ -s "$broken_tmp" ]; then + echo "dangling symlinks found under $root" >&2 + cat "$broken_tmp" >&2 + rm -f "$broken_tmp" + exit 1 + fi + rm -f "$broken_tmp" +} + mkdir -p "$out/lib/openclaw" "$out/bin" -log_step "copy build outputs" cp -r dist node_modules package.json "$out/lib/openclaw/" +# Build dir is ephemeral in Nix; moving avoids an expensive deep copy of node_modules. +log_step "move build outputs" mv dist node_modules package.json "$out/lib/openclaw/" if [ -d extensions ]; then log_step "copy extensions" cp -r extensions "$out/lib/openclaw/" fi @@ -94,4 +112,6 @@ if [ -n "$hasown_src" ]; then fi fi +log_step "validate node_modules symlinks" check_no_broken_symlinks "$out/lib/openclaw/node_modules" + bash -e -c '. "$STDENV_SETUP"; makeWrapper "$NODE_BIN" "$out/bin/openclaw" --add-flags "$out/lib/openclaw/dist/index.js" --set-default OPENCLAW_NIX_MODE "1"' From 9dc336c6f572b6d0a212a0cd9198a04ea6dcd83f Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 15 Feb 2026 21:18:18 -0500 Subject: [PATCH 2/3] nix: harden gateway symlink and smoke scripts --- nix/scripts/check-gateway-smoke.sh | 4 +++- nix/scripts/gateway-install.sh | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/nix/scripts/check-gateway-smoke.sh b/nix/scripts/check-gateway-smoke.sh index aa38254d..3dffddc9 100755 --- a/nix/scripts/check-gateway-smoke.sh +++ b/nix/scripts/check-gateway-smoke.sh @@ -6,6 +6,8 @@ if [ -z "${OPENCLAW_GATEWAY:-}" ]; then exit 1 fi -export HOME="$(mktemp -d)" +tmp_home="$(mktemp -d)" +export HOME="$tmp_home" +trap 'rm -rf "$tmp_home"' EXIT "$OPENCLAW_GATEWAY/bin/openclaw" --help >/dev/null diff --git a/nix/scripts/gateway-install.sh b/nix/scripts/gateway-install.sh index b7f239be..ee6dd139 100755 --- a/nix/scripts/gateway-install.sh +++ b/nix/scripts/gateway-install.sh @@ -24,12 +24,15 @@ check_no_broken_symlinks() { fi broken_tmp="$(mktemp)" - find "$root" -type l ! -exec test -e {} \; -print > "$broken_tmp" + # Portable and faster than `find ... -exec test -e {} \;` on large trees. + find "$root" -type l -print | while IFS= read -r link; do + [ -e "$link" ] || printf '%s\n' "$link" + done > "$broken_tmp" if [ -s "$broken_tmp" ]; then echo "dangling symlinks found under $root" >&2 cat "$broken_tmp" >&2 rm -f "$broken_tmp" - exit 1 + return 1 fi rm -f "$broken_tmp" } From 1d88dceda6c23f0c526c7951bbd5fd127682360e Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 15 Feb 2026 20:19:19 -0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20nix:=20trim=20PR=20#57=20to?= =?UTF-8?q?=20packaging-only=20speedups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What: - revert cache pipeline producer/consumer workflow orchestration in `.github/workflows/cache-only.yml` - revert duplicated Garnix wait/polling logic from config-options and hm-activation workflows - remove gateway-smoke check wiring from `flake.nix` - delete `nix/checks/openclaw-gateway-smoke.nix` and `nix/scripts/check-gateway-smoke.sh` Why: - keep PR #57 minimal and focused on high-value gateway packaging/install speedups - avoid non-essential CI complexity and SHA/polling failure surface - preserve only core build-time optimizations (`dontFixup`, install `mv`, symlink integrity guard) Tests: - `nix build .#checks.x86_64-linux.gateway --print-build-logs` (pass) - `nix flake check --accept-flake-config --print-build-logs` (pass; warns about omitted incompatible systems) --- .github/workflows/cache-only.yml | 80 +--------------------- .github/workflows/config-options-check.yml | 36 +--------- .github/workflows/hm-activation-linux.yml | 36 +--------- flake.nix | 3 - nix/checks/openclaw-gateway-smoke.nix | 22 ------ nix/scripts/check-gateway-smoke.sh | 13 ---- 6 files changed, 5 insertions(+), 185 deletions(-) delete mode 100644 nix/checks/openclaw-gateway-smoke.nix delete mode 100755 nix/scripts/check-gateway-smoke.sh diff --git a/.github/workflows/cache-only.yml b/.github/workflows/cache-only.yml index 59cdd457..452209ce 100644 --- a/.github/workflows/cache-only.yml +++ b/.github/workflows/cache-only.yml @@ -1,4 +1,4 @@ -name: Cache Pipeline +name: Cache Only on: pull_request: @@ -9,8 +9,8 @@ on: types: [ completed ] jobs: - producer-cache-ready: - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} + cache-only: + if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} runs-on: ubuntu-latest env: TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} @@ -68,12 +68,9 @@ jobs: packages.x86_64-linux.openclaw-gateway packages.x86_64-linux.openclaw-tools checks.aarch64-darwin.gateway - checks.aarch64-darwin.gateway-smoke checks.x86_64-linux.gateway - checks.x86_64-linux.gateway-smoke checks.x86_64-linux.gateway-tests checks.x86_64-linux.config-options - checks.x86_64-linux.hm-activation ) deadline=$(( $(date +%s) + WAIT_MINUTES * 60 )) @@ -101,74 +98,3 @@ jobs: echo "Cache still missing (${#missing[@]}). Retrying in 30s..." sleep 30 done - - consumer-cache-only: - if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} - runs-on: ubuntu-latest - env: - TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ env.TARGET_SHA }} - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v13 - - - name: Wait for Garnix checks - uses: actions/github-script@v7 - with: - script: | - const waitMinutes = 30 - const intervalMs = 30_000 - const deadline = Date.now() + waitMinutes * 60 * 1000 - const targetSha = process.env.TARGET_SHA || context.sha - - while (true) { - const { data } = await github.rest.checks.listForRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: targetSha, - }) - const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') - if (garnix && garnix.status === 'completed') { - if (garnix.conclusion !== 'success') { - core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) - } - break - } - if (Date.now() > deadline) { - core.setFailed('Timed out waiting for Garnix checks') - break - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } - - - name: Consume Linux checks from cache only - run: | - nix build --extra-experimental-features 'nix-command flakes' \ - --accept-flake-config \ - --max-jobs 0 \ - .#checks.x86_64-linux.ci \ - --print-build-logs - - - name: Consume Darwin artifacts from cache only - env: - STORE_URL: https://cache.garnix.io - run: | - set -euo pipefail - - targets=( - packages.aarch64-darwin.openclaw - packages.aarch64-darwin.openclaw-gateway - packages.aarch64-darwin.openclaw-tools - packages.aarch64-darwin.openclaw-app - checks.aarch64-darwin.gateway - checks.aarch64-darwin.gateway-smoke - ) - - for target in "${targets[@]}"; do - out_path=$(nix --extra-experimental-features 'nix-command flakes' eval --accept-flake-config --raw ".#${target}.outPath") - nix path-info --store "$STORE_URL" "$out_path" >/dev/null - done diff --git a/.github/workflows/config-options-check.yml b/.github/workflows/config-options-check.yml index 13f95a7d..d479fb47 100644 --- a/.github/workflows/config-options-check.yml +++ b/.github/workflows/config-options-check.yml @@ -26,39 +26,5 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@v13 - - name: Wait for Garnix checks - uses: actions/github-script@v7 - with: - script: | - const waitMinutes = 30 - const intervalMs = 30_000 - const deadline = Date.now() + waitMinutes * 60 * 1000 - const targetSha = process.env.TARGET_SHA || context.sha - - while (true) { - const { data } = await github.rest.checks.listForRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: targetSha, - }) - const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') - if (garnix && garnix.status === 'completed') { - if (garnix.conclusion !== 'success') { - core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) - } - break - } - if (Date.now() > deadline) { - core.setFailed('Timed out waiting for Garnix checks') - break - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } - - name: Verify config options are up to date - run: | - nix build --extra-experimental-features 'nix-command flakes' \ - --accept-flake-config \ - --max-jobs 0 \ - .#checks.x86_64-linux.config-options \ - --print-build-logs + run: nix build .#checks.x86_64-linux.config-options --print-build-logs diff --git a/.github/workflows/hm-activation-linux.yml b/.github/workflows/hm-activation-linux.yml index 7bfc0518..9b657f2c 100644 --- a/.github/workflows/hm-activation-linux.yml +++ b/.github/workflows/hm-activation-linux.yml @@ -23,39 +23,5 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@v13 - - name: Wait for Garnix checks - uses: actions/github-script@v7 - with: - script: | - const waitMinutes = 30 - const intervalMs = 30_000 - const deadline = Date.now() + waitMinutes * 60 * 1000 - const targetSha = process.env.TARGET_SHA || context.sha - - while (true) { - const { data } = await github.rest.checks.listForRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: targetSha, - }) - const garnix = data.check_runs.find((run) => run.name === 'All Garnix checks') - if (garnix && garnix.status === 'completed') { - if (garnix.conclusion !== 'success') { - core.setFailed(`Garnix checks not successful: ${garnix.conclusion}`) - } - break - } - if (Date.now() > deadline) { - core.setFailed('Timed out waiting for Garnix checks') - break - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } - - name: Run HM activation - run: | - nix build --extra-experimental-features 'nix-command flakes' \ - --accept-flake-config \ - --max-jobs 0 \ - .#checks.x86_64-linux.hm-activation \ - --print-build-logs + run: nix build .#checks.x86_64-linux.hm-activation --print-build-logs diff --git a/flake.nix b/flake.nix index 39c4f913..6238ebac 100644 --- a/flake.nix +++ b/flake.nix @@ -69,9 +69,6 @@ let baseChecks = { gateway = packageSetStable.openclaw-gateway; - gateway-smoke = pkgs.callPackage ./nix/checks/openclaw-gateway-smoke.nix { - openclawGateway = packageSetStable.openclaw-gateway; - }; package-contents = pkgs.callPackage ./nix/checks/openclaw-package-contents.nix { openclawGateway = packageSetStable.openclaw-gateway; }; diff --git a/nix/checks/openclaw-gateway-smoke.nix b/nix/checks/openclaw-gateway-smoke.nix deleted file mode 100644 index 2d6944fe..00000000 --- a/nix/checks/openclaw-gateway-smoke.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ - lib, - stdenv, - openclawGateway, -}: - -stdenv.mkDerivation { - pname = "openclaw-gateway-smoke"; - version = lib.getVersion openclawGateway; - - dontUnpack = true; - dontConfigure = true; - dontBuild = true; - - env = { - OPENCLAW_GATEWAY = openclawGateway; - }; - - doCheck = true; - checkPhase = "${../scripts/check-gateway-smoke.sh}"; - installPhase = "${../scripts/empty-install.sh}"; -} diff --git a/nix/scripts/check-gateway-smoke.sh b/nix/scripts/check-gateway-smoke.sh deleted file mode 100755 index 3dffddc9..00000000 --- a/nix/scripts/check-gateway-smoke.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -e - -if [ -z "${OPENCLAW_GATEWAY:-}" ]; then - echo "OPENCLAW_GATEWAY is not set" >&2 - exit 1 -fi - -tmp_home="$(mktemp -d)" -export HOME="$tmp_home" -trap 'rm -rf "$tmp_home"' EXIT - -"$OPENCLAW_GATEWAY/bin/openclaw" --help >/dev/null