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.
This commit is contained in:
Daniel Han 2026-05-16 06:04:28 +00:00
commit 883f26a2a4
3 changed files with 24 additions and 9 deletions

View file

@ -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

View file

@ -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

View file

@ -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"