Files
openclaw/.github/actions/setup-node-env/action.yml
Peter Steinberger 6252f9b1f7 improve(ci): speed warm Node shards with sticky bind mount (#108386)
* perf(ci): bind sticky dependency disk into test shards

* chore(ci): restore workflow guard coverage

* perf(ci): skip pnpm install on exact sticky hits

* fix(ci): require stock layout for sticky dependencies

* perf(ci): bypass pnpm for test shard launches

* fix(ci): declare direct shard runner command

* fix(ci): bypass pnpm in warm shard builds

* fix(ci): invalidate sticky deps on pnpm config

* ci: document sticky failure guard
2026-07-15 15:41:24 -07:00

227 lines
9.0 KiB
YAML

name: Setup Node environment
description: >
Install Node 24 by default, pnpm, optionally Bun, and optionally run pnpm
install. Requires actions/checkout to run first.
inputs:
node-version:
description: Node.js version to install.
required: false
default: "24.x"
install-bun:
description: Whether to install Bun alongside Node.
required: false
default: "true"
install-deps:
description: Whether to run pnpm install after environment setup.
required: false
default: "true"
frozen-lockfile:
description: Whether to use --frozen-lockfile for install.
required: false
default: "true"
use-actions-cache:
description: Whether to restore the pnpm store with actions/cache.
required: false
default: "true"
save-actions-cache:
description: Whether to save the pnpm store with actions/cache after install when no exact cache restored.
required: false
default: "false"
sticky-disk:
description: >
Mount a Blacksmith sticky disk for the pnpm store and bind its
node_modules directory over the checkout's stock node_modules path.
Only valid on Blacksmith Linux runners; pass use-actions-cache: "false"
alongside.
required: false
default: "false"
runs:
using: composite
steps:
- name: Normalize container toolcache
shell: bash
run: |
set -euo pipefail
if [[ -d /__t && ! -e /opt/hostedtoolcache ]]; then
mkdir -p /opt
ln -s /__t /opt/hostedtoolcache
fi
- name: Setup Node.js
shell: bash
env:
REQUESTED_NODE_VERSION: ${{ inputs.node-version }}
run: |
set -euo pipefail
source "$GITHUB_ACTION_PATH/../setup-pnpm-store-cache/ensure-node.sh"
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Setup pnpm
id: setup-pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ inputs.node-version }}
use-actions-cache: ${{ inputs.use-actions-cache }}
- name: Validate sticky pnpm layout
if: inputs.sticky-disk == 'true'
shell: bash
run: |
set -euo pipefail
for config_name in modules-dir virtual-store-dir; do
config_value="$(pnpm config get "$config_name")"
case "$config_value" in
""|undefined|null) ;;
*)
echo "::error::$config_name must be unset when sticky-disk is enabled; sticky mode requires pnpm's stock node_modules layout"
exit 2
;;
esac
done
- name: Mount dependency sticky disk
if: inputs.sticky-disk == 'true'
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
with:
# The key covers every dependency and lifecycle input. PR scope keeps
# feature snapshots separate from protected pushes. The v2 marker lets
# exact warm hits trust the snapshotted stock layout without rescanning.
key: ${{ github.repository }}-node-deps-bind-v2-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'protected' }}-${{ inputs.node-version }}-frozen-${{ inputs.frozen-lockfile }}-${{ hashFiles('**/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', 'pnpmfile.cjs', '.github/actions/setup-node-env/sticky-importers.sh', 'scripts/postinstall-bundled-plugins.mjs', 'scripts/preinstall-package-manager-warning.mjs', 'scripts/prepare-git-hooks.mjs') }}
path: /var/tmp/openclaw-node-deps
# v1.4.0 skips commit after failed/cancelled steps or ambiguous failure
# detection, so an incomplete first hydration cannot seed this key.
commit: if-missing
# Post actions run last-in-first-out. Register after stickydisk so this bind
# is gone before stickydisk flushes, unmounts, and snapshots its filesystem.
- name: Register sticky bind cleanup
if: inputs.sticky-disk == 'true'
uses: ./.github/actions/register-bind-mount-cleanup
with:
path: ${{ github.workspace }}/node_modules
- name: Bind sticky node_modules into workspace
if: inputs.sticky-disk == 'true'
shell: bash
run: |
set -euo pipefail
sticky_root=/var/tmp/openclaw-node-deps
sticky_modules="$sticky_root/node_modules"
sticky_store="$sticky_root/store"
workspace_modules="$GITHUB_WORKSPACE/node_modules"
mkdir -p "$sticky_modules" "$sticky_store" "$workspace_modules"
sudo mount --bind "$sticky_modules" "$workspace_modules"
mountpoint -q "$workspace_modules"
if [ "$(stat -c %d "$sticky_store")" != "$(stat -c %d "$workspace_modules")" ]; then
echo "::error::pnpm store and workspace node_modules must share a filesystem"
exit 1
fi
findmnt --target "$workspace_modules"
# zizmor: ignore[github-env] static trusted path defined in this composite action.
echo "PNPM_CONFIG_STORE_DIR=$sticky_store" >> "$GITHUB_ENV"
# pnpm exec may reconcile a restored workspace before nested builds.
# Sticky jobs already have every build tool, so invoke their Node entrypoints directly.
echo "OPENCLAW_BUILD_ALL_NO_PNPM=1" >> "$GITHUB_ENV"
- name: Setup Bun
if: inputs.install-bun == 'true'
shell: bash
run: |
set -euo pipefail
npm install -g bun@1.3.14
- name: Runtime versions
shell: bash
run: |
node -v
npm -v
pnpm -v
if command -v bun &>/dev/null; then bun -v; fi
- name: Capture node path
shell: bash
run: |
node_bin="$(dirname "$(node -p 'process.execPath')")"
if command -v cygpath >/dev/null 2>&1; then
node_bin="$(cygpath -u "$node_bin")"
fi
# zizmor: ignore[github-env] node_bin comes from trusted actions/setup-node output in this composite action.
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
echo "$node_bin" >> "$GITHUB_PATH"
- name: Install dependencies
if: inputs.install-deps == 'true'
shell: bash
env:
CI: "true"
FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }}
STICKY_DISK: ${{ inputs.sticky-disk }}
STICKY_ROOT: /var/tmp/openclaw-node-deps
run: |
set -euo pipefail
export PATH="$NODE_BIN:$PATH"
which node
node -v
pnpm -v
case "$FROZEN_LOCKFILE" in
true) LOCKFILE_FLAG="--frozen-lockfile" ;;
false) LOCKFILE_FLAG="" ;;
*)
echo "::error::Invalid frozen-lockfile input: '$FROZEN_LOCKFILE' (expected true or false)"
exit 2
;;
esac
install_args=(
install
--prefer-offline
--ignore-scripts=false
--config.engine-strict=false
--config.enable-pre-post-scripts=true
--config.side-effects-cache=true
)
if [ -n "$LOCKFILE_FLAG" ]; then
install_args+=("$LOCKFILE_FLAG")
fi
append_pnpm_option_arg() {
local env_name="$1"
local option_name="$2"
local value="${!env_name-}"
if [ -n "$value" ]; then
install_args+=("--${option_name}=${value}")
fi
}
append_pnpm_option_arg PNPM_CONFIG_CHILD_CONCURRENCY child-concurrency
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
append_pnpm_option_arg PNPM_CONFIG_STORE_DIR store-dir
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
export NODE_PATH="$PNPM_CONFIG_MODULES_DIR${NODE_PATH:+:$NODE_PATH}"
fi
sticky_ready_marker="$STICKY_ROOT/.install-complete-v2"
if [ "$STICKY_DISK" = "true" ] && [ -f "$sticky_ready_marker" ]; then
bash "$GITHUB_ACTION_PATH/sticky-importers.sh" restore "$STICKY_ROOT" "$GITHUB_WORKSPACE"
echo "Sticky dependency snapshot is ready; skipping pnpm install"
else
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
rm -rf node_modules
ln -sfn "$PNPM_CONFIG_MODULES_DIR" node_modules
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi
if [ "$STICKY_DISK" = "true" ]; then
bash "$GITHUB_ACTION_PATH/sticky-importers.sh" capture "$STICKY_ROOT" "$GITHUB_WORKSPACE"
fi
fi
- name: Save pnpm store cache
if: ${{ inputs.install-deps == 'true' && inputs.use-actions-cache == 'true' && inputs.save-actions-cache == 'true' && runner.os != 'Windows' && steps.setup-pnpm.outputs.store-cache-hit != 'true' }}
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ steps.setup-pnpm.outputs.store-path }}
key: ${{ steps.setup-pnpm.outputs.store-cache-primary-key }}