From 883f26a2a4f8beb9da2e9a68e39c61a8108e831b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 16 May 2026 06:04:28 +0000 Subject: [PATCH] ci: pin install to lockfile in remaining install paths Followup to 152fe8d. Three more sites still called naked `bun install` / `npm install`, which honour caret ranges in package.json and can pull a fresh minor/patch of a transitive dep from the registry on the next run. studio/setup.ps1 (4 sites): the Windows end-user installer. bun install -> bun install --frozen-lockfile (both initial and the cache-clear retry); the npm fallback and the OXC validator npm install both -> npm ci. Error messages updated to reference the new command. studio/setup.sh: the OXC validator runtime install for the Unix path was still naked `npm install`. Now `npm ci`. github/workflows/release-desktop.yml: the desktop release build's frontend install was still naked `npm install`. Now `npm ci` so a published .app/.dmg/.AppImage/.msi can never have shipped with a registry-resolved transitive that drifted from the committed lockfile. The pinned Tauri CLI install in the same workflow stays as `npm install --save-dev @tauri-apps/cli@2.10.1` because that line is intentionally adding a specific package to package.json, not syncing from the lockfile. Verified `npm ci --no-fund --no-audit --dry-run` exits 0 against both the studio/frontend and studio/backend/core/data_recipe/ oxc-validator lockfiles. --- .github/workflows/release-desktop.yml | 6 +++++- studio/setup.ps1 | 23 ++++++++++++++++------- studio/setup.sh | 4 +++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 810bb644ba..7eb3da4095 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -443,13 +443,17 @@ jobs: - name: Install frontend dependencies working-directory: studio/frontend + # `npm ci` so a release build can never pull a fresh minor/patch + # of a transitive dep from the registry via caret-range resolution; + # the tree is exactly what the committed lockfile pins. + # # Lifecycle scripts (esbuild native-binary postinstall, etc.) are # required for `vite build`. The pre-install lockfile structural # audit (lockfile_supply_chain_audit.py) is the practical defence # against the npm postinstall-dropper class -- it fires BEFORE any # tarball runs, on the injection pattern itself rather than an # advisory-DB lookup. - run: npm install --no-fund --no-audit + run: npm ci --no-fund --no-audit # ── Rust ── - name: Install Rust stable diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 40788a0ecb..095c767c4d 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1333,9 +1333,13 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { # metadata but no actual content (bin/, lib/). When this happens bun install # exits 0 but leaves binaries missing. We validate after install and clear # the cache + retry once before falling back to npm. + # + # --frozen-lockfile so a Windows end-user install can never pull a fresh + # minor/patch of a transitive dep from the registry via caret-range + # resolution; the tree is exactly what the committed lockfile pins. if ($UseBun) { Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile } # On Windows, .bin/ entries vary by package manager: # npm → tsc, tsc.cmd, tsc.ps1 # bun → tsc.exe, tsc.bunx @@ -1349,7 +1353,7 @@ 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 } $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) { @@ -1368,13 +1372,16 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } } if (-not $UseBun) { - $npmExit = Invoke-SetupCommand { npm install } + # npm ci (not npm install) so the install is pinned to the committed + # lockfile -- a hijacked transitive cannot land via caret-range + # resolution. Fails fast if package.json and the lockfile have drifted. + $npmExit = Invoke-SetupCommand { npm ci } 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,11 +1418,13 @@ if (Test-Path $OxcValidatorDir) { $prevEAP_oxc = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $OxcValidatorDir - $oxcInstallExit = Invoke-SetupCommand { npm install } + # npm ci pins the oxc validator install to its committed lockfile so a + # hijacked transitive cannot land via caret-range resolution. + $oxcInstallExit = Invoke-SetupCommand { npm ci } if ($oxcInstallExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_oxc - Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red + Write-Host "[ERROR] OXC validator npm ci failed (exit code $oxcInstallExit)" -ForegroundColor Red exit 1 } Pop-Location diff --git a/studio/setup.sh b/studio/setup.sh index 8511cf5822..6d71b68319 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -417,7 +417,9 @@ fi # end frontend build check # ── oxc-validator runtime ── 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 + # npm ci pins the oxc validator install to its committed lockfile so a + # hijacked transitive cannot land via caret-range resolution. + 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"