diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 420b26b59d49..27971a484f63 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -23,6 +23,39 @@ runs: - name: Install pnpm uses: ./.github/actions/install-pnpm + # When the node_modules layout changes, `pnpm install` keeps the `.bin` scripts of the workspace packages, which + # then point into a folder that the new layout does not have. Branches that change `nodeLinker` share this cache + # volume, so start from empty node_modules folders when the layout on the volume differs, or when an earlier + # install already left such broken `.bin` scripts behind. + - name: Remove node_modules of another layout + run: | + mapfile -t dirs < <(grep '/node_modules$' <<< "$DEPENDENCY_PATHS") + expected=$(pnpm config get node-linker) + if [[ "$expected" == undefined ]]; then + expected=isolated + fi + actual=$(node -p 'try { JSON.parse(require("fs").readFileSync("node_modules/.modules.yaml", "utf8")).nodeLinker ?? "" } catch { "" }') + if [[ -n "$actual" && "$actual" != "$expected" ]]; then + reason="The node_modules folders use the $actual layout, but nodeLinker is $expected." + else + broken=$(node "$GITHUB_ACTION_PATH/find-broken-bin.mjs" "${dirs[@]}") + if [[ -n "$broken" ]]; then + reason="The target of $broken does not exist." + fi + fi + if [[ -n "$reason" ]]; then + echo "$reason Removing the node_modules folders." + for dir in "${dirs[@]}"; do + if [[ -d "$dir" ]]; then + # The folders are mounted from the cache volume, so empty them instead of removing them + find "$dir/" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + fi + done + fi + env: + DEPENDENCY_PATHS: ${{ steps.paths.outputs.paths }} + shell: bash + # The cache volume can hold node_modules of another commit or branch. This is a no-op when they match # pnpm-lock.yaml. - name: Install dependencies diff --git a/.github/actions/install-dependencies/find-broken-bin.mjs b/.github/actions/install-dependencies/find-broken-bin.mjs new file mode 100644 index 000000000000..24aa8753cac5 --- /dev/null +++ b/.github/actions/install-dependencies/find-broken-bin.mjs @@ -0,0 +1,34 @@ +// Prints the first `.bin` entry in the given node_modules folders whose target does not exist. +import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +// The pnpm shell shim runs the target with `exec "$basedir_abs/"` (`$basedir` in older versions) +const SHIM_TARGET = /^\s*exec \S+\s+"\$basedir(?:_abs)?\/([^"]+)"/m; + +for (const dir of process.argv.slice(2)) { + const binDir = join(dir, '.bin'); + let names; + try { + names = readdirSync(binDir); + } catch { + continue; + } + + for (const name of names) { + const bin = join(binDir, name); + const stat = lstatSync(bin); + let target; + if (stat.isSymbolicLink()) { + target = bin; + } else if (stat.isFile() && stat.size < 64 * 1024) { + const match = readFileSync(bin, 'utf8').match(SHIM_TARGET); + target = match && resolve(binDir, match[1]); + } + + if (target && !existsSync(target)) { + // eslint-disable-next-line no-console + console.log(bin); + process.exit(0); + } + } +}