fix(ci): cp -r instead of cp -al — /opt and /workspace are different filesystems

The hardlink-copy fix landed and immediately broke with:
  cp: cannot create hard link 'node_modules/<file>' to
      '/opt/node_modules_cache/<file>': Invalid cross-device link

GitHub Actions runners mount the workspace volume at /workspace
(overlay-fs layered onto the runner image), and /opt is the runner
image's own filesystem. Cross-filesystem hardlinks aren't supported.

Switch `cp -al` → `cp -r`. Cost: ~5s for ~200 packages of small JS
files vs ~0s for the broken symlink. Still cheaper than the ~15s
`bun install` fallback. Realpath of /workspace/node_modules/<pkg>/...
stays inside /workspace, so bun build's sibling-dep resolution works.

Both evals.yml and evals-periodic.yml updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-05-07 16:00:02 -07:00
parent 999aefb472
commit 0492283610
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 15 additions and 16 deletions

View File

@ -101,14 +101,14 @@ jobs:
echo "TMPDIR=/home/runner/.cache" echo "TMPDIR=/home/runner/.cache"
} >> "$GITHUB_ENV" } >> "$GITHUB_ENV"
# Hardlink copy (cp -al) instead of symlink: bun build resolves a file's # Recursive copy (cp -r) instead of symlink: bun build resolves a
# realpath when looking for sibling deps, which makes a symlinked # file's realpath when looking for sibling deps. See evals.yml for the
# /opt/node_modules_cache fail to resolve transitive deps. See # full explanation. cp -al would be faster but /opt and /workspace
# evals.yml for the full explanation. # are on different overlay-fs layers, so cross-device hardlink fails.
- name: Restore deps - name: Restore deps
run: | run: |
if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then
cp -al /opt/node_modules_cache node_modules cp -r /opt/node_modules_cache node_modules
else else
bun install bun install
fi fi

View File

@ -110,20 +110,19 @@ jobs:
echo "TMPDIR=/home/runner/.cache" echo "TMPDIR=/home/runner/.cache"
} >> "$GITHUB_ENV" } >> "$GITHUB_ENV"
# Restore pre-installed node_modules from Docker image via hardlink copy. # Restore pre-installed node_modules from Docker image via recursive
# Why hardlinks instead of symlink: bun build (and Node module resolution # copy. Symlink (`ln -s`) breaks bun's module resolution because bun
# in general) resolves a file's realpath when walking up to find # resolves a file's realpath when walking up to find node_modules/<dep>;
# node_modules/<dep>. With `ln -s /opt/node_modules_cache node_modules`, # from a symlinked path, realpath escapes the workspace and sibling
# resolution from inside socks/build/client/socksclient.js walks the # deps no longer resolve. Hardlink copy (`cp -al`) fails because /opt
# /opt/node_modules_cache/... realpath, where there is no parent # and /workspace are on different overlay-fs layers ("Invalid
# node_modules dir containing smart-buffer/ip-address. With `cp -al`, # cross-device link"). Recursive copy works on every layout. Cost:
# each file's realpath is /workspace/node_modules/..., so sibling deps # ~5s for ~200 packages of small JS files vs ~0s for symlink — still
# resolve correctly. Speed is comparable to symlink (hardlinks share # vastly cheaper than rerunning `bun install` (network + resolution).
# inodes, no actual data copy).
- name: Restore deps - name: Restore deps
run: | run: |
if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then
cp -al /opt/node_modules_cache node_modules cp -r /opt/node_modules_cache node_modules
else else
bun install bun install
fi fi