name: Install Node dependencies description: Installs the Node toolchain and repository dependencies for CI jobs, with optional Electron archive caching. inputs: cache-pnpm-store: description: Restore or save the pnpm download store; verification and native caches are independent. required: false default: 'true' cache-pnpm-store-lookup-only: description: Auto uses measured hosted Node 24 root installs; true forces lookup, false retains archive restoration. required: false default: auto cache-pnpm-verification: description: Restore pnpm's policy-checked lockfile verification record. required: false default: 'true' native-runtime: description: Native runtime to prepare after the script-free install (none, node, or electron). required: false default: none node-version: description: Node.js version override; defaults to the version declared in package.json. required: false default: '' cache-dependency-path: description: Lockfiles for the pnpm download store; include mobile/pnpm-lock.yaml only when the job installs mobile dependencies. required: false default: pnpm-lock.yaml persist-native-cache: description: Save restored native modules at job end. Set false when a later step overwrites the same path with a different ABI. required: false default: 'true' cache-electron-package: description: Cache the Electron package archive and export ELECTRON_CACHE for following steps. required: false default: 'false' outputs: pnpm-store-cache-hit: description: Whether the requested download store matched an existing cache. value: ${{ steps.pnpm-store-lookup.outputs.cache-hit || steps.pnpm-store-restore.outputs.cache-hit || steps.requested-node.outputs.cache-hit || steps.default-node.outputs.cache-hit }} verification-cache-hit: description: Whether pnpm's verification record was restored. value: ${{ steps.verification-cache.outputs.cache-hit }} verification-cache-path: description: The small pnpm-owned verification record, without registry metadata. value: ${{ steps.verification-cache.outputs.path }} verification-cache-key: description: Exact verification record cache key. value: ${{ steps.verification-cache.outputs.key }} node-version: description: Resolved Node.js version used for the install. value: ${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }} native-cache-scope: description: Operating-system image scope used by the native module cache. value: ${{ steps.native-runtime.outputs.cache-scope }} native-cache-key: description: Exact native cache key requested after installation. value: ${{ steps.native-runtime.outputs.cache-key }} native-cache-path: description: Native build directories covered by the requested cache. value: ${{ steps.native-runtime.outputs.cache-path }} native-cache-hit: description: Whether the compiled native module cache was restored. value: ${{ steps.native-runtime.outputs.cache-hit }} runs: using: composite steps: # The image resets HOME during login; shell fixtures intentionally use temporary homes. - name: Preserve explicit login-shell homes on Blacksmith if: runner.os == 'Linux' && startsWith(runner.name, 'blacksmith-') && job.container.id == '' shell: bash run: | sudo python3 - <<'PYTHON' from pathlib import Path profile = Path('/etc/profile.d/blacksmith.sh') if profile.exists(): original = profile.read_text() updated = original.replace('export HOME="$RUNNER_HOME"', 'export HOME="${HOME:-$RUNNER_HOME}"') if updated != original: profile.write_text(updated) PYTHON fixture_home="$(mktemp -d)" trap 'rm -rf "$fixture_home"' EXIT printf 'export ORCA_CI_PROFILE_LOADED=1\n' > "$fixture_home/.bash_profile" HOME="$fixture_home" ORCA_CI_FIXTURE_HOME="$fixture_home" /bin/bash -lc ' test "$HOME" = "$ORCA_CI_FIXTURE_HOME" && test "$ORCA_CI_PROFILE_LOADED" = 1 ' || { echo '::error::Runner login profile overrides fixture HOME' grep -nH 'HOME' /etc/profile /etc/profile.d/*.sh || true HOME="$fixture_home" /bin/bash -lc 'printf "Observed HOME=%s\n" "$HOME"' exit 1 } - name: Resolve pnpm store mode id: pnpm-store-mode if: >- github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && (inputs.cache-pnpm-store-lookup-only == 'true' || (inputs.cache-pnpm-store-lookup-only == 'auto' && inputs.cache-dependency-path == 'pnpm-lock.yaml' && runner.environment == 'github-hosted' && job.container.id == '' && (runner.os == 'Linux' || runner.os == 'macOS' || runner.os == 'Windows') && (runner.arch == 'X64' || runner.arch == 'ARM64') && (inputs.node-version == '' || inputs.node-version == '24'))) shell: bash env: LOOKUP_REQUEST: ${{ inputs.cache-pnpm-store-lookup-only }} run: | lookup_only=true case "$LOOKUP_REQUEST" in [aA][uU][tT][oO]) # Hosted runners have Node for this manifest-only check before toolchain setup. lookup_only="$(node -p 'const p = require("./package.json"); p.engines?.node === "24" && typeof p.packageManager === "string" && p.packageManager.split("+")[0] === "pnpm@12.14.0"')" ;; esac printf 'lookup-only=%s\n' "$lookup_only" >> "$GITHUB_OUTPUT" # setup-node needs pnpm on PATH to locate and restore its store. - name: Setup pnpm uses: ./.github/actions/setup-pnpm # Desktop-only jobs should not miss their download cache when mobile dependencies change. - name: Setup Node.js id: default-node if: inputs.node-version == '' uses: actions/setup-node@v6 with: node-version-file: package.json check-latest: true cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false - name: Setup requested Node.js id: requested-node if: inputs.node-version != '' uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} check-latest: true cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false # PR-local stores compete with reusable build caches for the repository quota. - name: Resolve pnpm download store id: pnpm-store if: >- github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) || (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only == 'true') shell: bash env: LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }} STORE_LOOKUP_ONLY: ${{ steps.pnpm-store-mode.outputs.lookup-only == 'true' }} run: | test -n "$LOCKFILE_HASH" cache_path="$(pnpm store path --silent)" test -n "$cache_path" printf 'path=%s\n' "$cache_path" >> "$GITHUB_OUTPUT" printf 'arch=%s\n' "$(node -p 'require("node:os").arch()')" >> "$GITHUB_OUTPUT" if [ "$STORE_LOOKUP_ONLY" = 'true' ]; then printf 'ORCA_PNPM_STORE_CACHE_PATH=%s\n' "$cache_path" >> "$GITHUB_ENV" fi # Match setup-node's key and path so existing default-branch stores remain reusable. # Direct downloads beat store restoration for the measured Linux, macOS and Windows installs. - name: Restore pnpm download store without saving id: pnpm-store-restore if: >- github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) uses: actions/cache/restore@v5 with: path: ${{ steps.pnpm-store.outputs.path }} key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }} # Producers can refresh access and publish misses without downloading existing archives. # Not on GitHub-hosted runners: nothing restores that store, and direct installs beat restoring it. - name: Keep pnpm download store without restoring id: pnpm-store-lookup if: steps.pnpm-store-mode.outputs.lookup-only == 'true' && runner.environment != 'github-hosted' uses: actions/cache@v5 with: # Twice-nested composite cleanup loses internal step outputs. path: ${{ env.ORCA_PNPM_STORE_CACHE_PATH }} key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }} lookup-only: true - name: Restore pnpm verification record id: verification-cache uses: ./.github/actions/restore-pnpm-verification with: enabled: ${{ inputs.cache-pnpm-verification }} - name: Validate native runtime shell: bash env: NATIVE_RUNTIME: ${{ inputs.native-runtime }} run: | case "$NATIVE_RUNTIME" in none|node|electron) ;; *) echo "::error::native-runtime must be none, node, or electron" exit 2 ;; esac - name: Prepare dependency install shell: bash run: | if [ -e node_modules ]; then ls -ld node_modules rm -rf node_modules fi # Frozen install avoids ~62MB/job of redundant resolution; retain diff guard to name stale lockfiles explicitly. - name: Install dependencies shell: bash run: | pnpm install --frozen-lockfile --ignore-scripts # Job containers can run composite steps from a source mirror without .git. if [ "$(git -C "$GITHUB_WORKSPACE" rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; then git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml fi # Why: the script-free install leaves node_modules/.bin/bun as a stub that shadows setup-bun under `pnpm run`. - name: Link pinned Bun shell: bash run: | if [ -d node_modules/bun ]; then pnpm rebuild bun fi # Why --if-present: release cuts run main's workflows against older release tags that predate this script. - name: Build internal packages shell: bash run: pnpm run --if-present build:packages # pnpm checks the cached record's policy and validity; never bypass verification. - name: Save pnpm verification record on main if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache.outputs.cache-hit != 'true' continue-on-error: true uses: actions/cache/save@v5 with: path: ${{ steps.verification-cache.outputs.path }} key: ${{ steps.verification-cache.outputs.key }} - name: Resolve Electron package cache id: electron-package-cache if: inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true' shell: bash run: | set -euo pipefail case "$RUNNER_OS" in Linux) cache_root="$HOME/.cache/electron" ;; macOS) cache_root="$HOME/Library/Caches/electron" ;; Windows) cache_root="${LOCALAPPDATA:-$HOME/AppData/Local}/electron/Cache" ;; *) echo "::error::Unsupported runner OS for Electron cache: $RUNNER_OS" exit 2 ;; esac printf 'cache-root=%s\n' "$cache_root" >> "$GITHUB_OUTPUT" printf 'ELECTRON_CACHE=%s\n' "$cache_root" >> "$GITHUB_ENV" printf 'version=%s\n' "$(node -p "require('./node_modules/electron/package.json').version")" >> "$GITHUB_OUTPUT" # Main and the warmers own every OS's archive; PR copies duplicated it per PR. - name: Cache Electron package archive if: github.event_name != 'pull_request' && steps.electron-package-cache.outputs.version != '' uses: actions/cache@v5 with: path: ${{ steps.electron-package-cache.outputs.cache-root }} key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }} - name: Restore Electron package archive without saving if: github.event_name == 'pull_request' && steps.electron-package-cache.outputs.version != '' uses: actions/cache/restore@v5 with: path: ${{ steps.electron-package-cache.outputs.cache-root }} key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }} - name: Prepare native runtime id: native-runtime if: inputs.native-runtime != 'none' uses: ./.github/actions/prepare-native-runtime with: native-runtime: ${{ inputs.native-runtime }} node-version: ${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }} persist-native-cache: ${{ inputs.persist-native-cache }}