mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* ci: narrow native, launcher and packaging cache keys and stop PR-local saves * ci: hash Rust crate sources, not build outputs, in launcher and hook keys * ci: fail closed on unlocked native packages and cover transitive key inputs * ci: resolve the Windows Rust cache scope with a script, not a nested local action --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
300 lines
14 KiB
YAML
300 lines
14 KiB
YAML
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 }}
|