diff --git a/build.sh b/build.sh index 1558dca240..cd984ceec1 100644 --- a/build.sh +++ b/build.sh @@ -33,18 +33,24 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -# Use bun for install if available (faster), fall back to npm. +# Lockfile-pinned install. Prefer bun when a bun.lock is checked in AND +# bun is on PATH; otherwise use `npm ci`, which is non-mutating and +# refuses to run unless the committed package-lock.json matches the +# dependency tree byte-for-byte. The `[ -f bun.lock ]` gate keeps bun +# off the path until a bun.lock is also committed -- without it, +# `bun install --frozen-lockfile` would fail and the script would +# still fall through to npm ci, so the gate just skips the noise. _install_ok=false -if command -v bun &>/dev/null; then - if bun install; then +if [ -f bun.lock ] && command -v bun &>/dev/null; then + if bun install --frozen-lockfile --no-progress; then _install_ok=true else - echo "⚠ bun install failed, falling back to npm" + echo "⚠ bun install --frozen-lockfile failed, falling back to npm ci" rm -rf node_modules fi fi if [ "$_install_ok" != "true" ]; then - if ! npm install; then + if ! npm ci --no-fund --no-audit; then echo "❌ ERROR: package install failed" >&2 exit 1 fi diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 16df87bbd5..8dd285a70a 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1321,13 +1321,19 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $WalkDir = Split-Path $WalkDir -Parent } - # Use bun if available (faster install), fall back to npm. - # Bun is used only as package manager; Node runs the actual build (Vite 8). + # Lockfile-pinned install. Prefer bun ONLY when a bun.lock is checked + # in AND bun is on PATH; otherwise use `npm ci`, which is non-mutating + # and refuses to run unless the committed package-lock.json matches + # the dependency tree byte-for-byte. The `Test-Path "bun.lock"` gate + # keeps bun off the path until a bun.lock is also committed -- the + # bun-fallback wiring (cache-clear retry + critical-binary + # verification) is kept intact so this script auto-upgrades when + # bun.lock eventually lands. $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir - $UseBun = $null -ne (Get-Command bun -ErrorAction SilentlyContinue) + $UseBun = (Test-Path "bun.lock") -and ($null -ne (Get-Command bun -ErrorAction SilentlyContinue)) # bun's package cache can become corrupt -- packages get stored with only # metadata but no actual content (bin/, lib/). When this happens bun install @@ -1335,7 +1341,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { # the cache + retry once before falling back to npm. if ($UseBun) { Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile --no-progress } # On Windows, .bin/ entries vary by package manager: # npm → tsc, tsc.cmd, tsc.ps1 # bun → tsc.exe, tsc.bunx @@ -1349,18 +1355,18 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } Invoke-SetupCommand { bun pm cache rm } | Out-Null - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile --no-progress } $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx") $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx") if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { - Write-Host " bun retry failed, falling back to npm" -ForegroundColor Yellow + Write-Host " bun retry failed, falling back to npm ci" -ForegroundColor Yellow if (Test-Path "node_modules") { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } $UseBun = $false } } else { - substep "bun install failed (exit $bunExit), falling back to npm" "Yellow" + substep "bun install --frozen-lockfile failed (exit $bunExit), falling back to npm ci" "Yellow" if (Test-Path "node_modules") { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } @@ -1368,13 +1374,13 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } } if (-not $UseBun) { - $npmExit = Invoke-SetupCommand { npm install } + $npmExit = Invoke-SetupCommand { npm ci --no-fund --no-audit } if ($npmExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } - Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red - Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow + Write-Host "[ERROR] npm ci failed (exit code $npmExit)" -ForegroundColor Red + Write-Host " Try running 'npm ci' manually in frontend/ to see errors" -ForegroundColor Yellow exit 1 } } @@ -1411,12 +1417,31 @@ if (Test-Path $OxcValidatorDir) { $prevEAP_oxc = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $OxcValidatorDir - $oxcInstallExit = Invoke-SetupCommand { npm install } - if ($oxcInstallExit -ne 0) { - Pop-Location - $ErrorActionPreference = $prevEAP_oxc - Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red - exit 1 + + # Same lockfile-pinned shape as the frontend block above: bun only + # when a bun.lock is checked in next to package.json (currently not + # the case), otherwise `npm ci` against the committed + # package-lock.json (shipped in #5604). + $UseOxcBun = (Test-Path "bun.lock") -and ($null -ne (Get-Command bun -ErrorAction SilentlyContinue)) + $oxcInstallOk = $false + if ($UseOxcBun) { + $oxcBunExit = Invoke-SetupCommand { bun install --frozen-lockfile --no-progress } + if ($oxcBunExit -eq 0) { + $oxcInstallOk = $true + } else { + if (Test-Path "node_modules") { + Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + if (-not $oxcInstallOk) { + $oxcInstallExit = Invoke-SetupCommand { npm ci --no-fund --no-audit } + if ($oxcInstallExit -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_oxc + Write-Host "[ERROR] OXC validator npm ci failed (exit code $oxcInstallExit)" -ForegroundColor Red + exit 1 + } } Pop-Location $ErrorActionPreference = $prevEAP_oxc diff --git a/studio/setup.sh b/studio/setup.sh index c5beb7ebd3..4adf42c8b2 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -329,21 +329,23 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -# Use bun for install if available (faster), fall back to npm. -# Build always uses npm (Node runtime -- avoids bun runtime issues on some platforms). -# NOTE: We intentionally avoid run_quiet for the bun install attempt because -# run_quiet calls exit on failure, which would kill the script before the npm -# fallback can run. Instead we capture output manually and only show it on failure. +# Lockfile-pinned install. Prefer bun ONLY when a bun.lock is checked in +# AND bun is on PATH; otherwise use `npm ci`, which is non-mutating and +# refuses to run unless the committed package-lock.json matches the +# dependency tree byte-for-byte. The `[ -f bun.lock ]` gate keeps bun +# off the path until a bun.lock is also committed -- the bun-fallback +# wiring (cache-clear retry + critical-binary verification) is kept +# intact so this script auto-upgrades when bun.lock eventually lands. # -# IMPORTANT: bun's package cache can become corrupt -- packages get stored -# with only metadata (package.json, README) but no actual content (bin/, -# lib/). When this happens bun install exits 0 but leaves binaries missing. -# We verify critical binaries after install. If missing, we clear the cache -# and retry once before falling back to npm. +# IMPORTANT: bun's package cache can become corrupt -- packages get +# stored with only metadata (package.json, README) but no actual content +# (bin/, lib/). When this happens bun install exits 0 but leaves +# binaries missing. We verify critical binaries after install. If +# missing, we clear the cache and retry once before falling back to npm. _try_bun_install() { local _log _exit_code=0 _log=$(mktemp) - bun install >"$_log" 2>&1 || _exit_code=$? + bun install --frozen-lockfile --no-progress >"$_log" 2>&1 || _exit_code=$? # bun may create .exe shims on Windows (Git Bash / MSYS2) instead of plain scripts if [ "$_exit_code" -eq 0 ] \ @@ -355,7 +357,7 @@ _try_bun_install() { # Either bun install failed or it exited 0 but left packages missing if [ "$_exit_code" -ne 0 ]; then - echo " bun install failed (exit code $_exit_code):" + echo " bun install --frozen-lockfile failed (exit code $_exit_code):" else echo " bun install exited 0 but critical binaries are missing:" fi @@ -366,7 +368,7 @@ _try_bun_install() { } _bun_install_ok=false -if command -v bun &>/dev/null; then +if [ -f bun.lock ] && command -v bun &>/dev/null; then substep "using bun for package install (faster)" if _try_bun_install; then _bun_install_ok=true @@ -381,7 +383,10 @@ if command -v bun &>/dev/null; then fi fi if [ "$_bun_install_ok" = false ]; then - run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error + # npm ci enforces the committed package-lock.json (refuses to install + # otherwise); --no-fund / --no-audit / --loglevel=error keep the + # output quiet during normal installs. + run_quiet_no_exit "npm ci" npm ci --no-fund --no-audit --loglevel=error _npm_install_rc=$? if [ "$_npm_install_rc" -ne 0 ]; then exit "$_npm_install_rc" @@ -406,12 +411,26 @@ cd "$SCRIPT_DIR" fi # end frontend build check # ── oxc-validator runtime ── +# Same lockfile-pinned shape as the frontend block above: bun only when +# a bun.lock is checked in next to package.json (currently not the +# case), otherwise `npm ci` against the committed package-lock.json +# (shipped in #5604). if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" - run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error - _oxc_install_rc=$? - if [ "$_oxc_install_rc" -ne 0 ]; then - exit "$_oxc_install_rc" + _oxc_install_ok=false + if [ -f bun.lock ] && command -v bun &>/dev/null; then + if run_quiet_no_exit "bun install (oxc validator runtime)" bun install --frozen-lockfile --no-progress; then + _oxc_install_ok=true + else + rm -rf node_modules + fi + fi + if [ "$_oxc_install_ok" = false ]; then + run_quiet_no_exit "npm ci (oxc validator runtime)" npm ci --no-fund --no-audit --loglevel=error + _oxc_install_rc=$? + if [ "$_oxc_install_rc" -ne 0 ]; then + exit "$_oxc_install_rc" + fi fi cd "$SCRIPT_DIR" fi