ci: address bot review on

Two issues codex flagged:

1. bun.lock gate (studio/frontend/.gitignore line 14 ignores bun.lock,
so it is never committed). bun install --frozen-lockfile cannot
migrate from package-lock.json, so without a bun.lock the bun path
always fails. setup.sh then misclassifies that as a corrupt cache,
clears the user's bun cache, and re-runs the same guaranteed-failing
command before falling back to npm. build.sh, studio/setup.sh, and
studio/setup.ps1 now only enter the bun path when bun.lock is
present; otherwise we go straight to npm ci.

2. OXC validator lockfile was outside the npm supply-chain scan
surface. lockfile_supply_chain_audit.py default, npm audit, OSV,
scan_npm_packages.py invocation, and the diff-for-new-install-scripts
step all now cover both lockfiles. security-audit.yml pull_request
paths filter triggers on changes to either. wheel-smoke checks the
built wheel ships the OXC lockfile too.

Verified:
python3 scripts/lockfile_supply_chain_audit.py
> OK: 0 findings across 2 npm + 1 cargo lockfile(s)
python3 scripts/scan_npm_packages.py --lockfile oxc-validator/...
> OK
This commit is contained in:
Daniel Han 2026-05-16 09:21:33 +00:00
commit d1124c52a9
6 changed files with 63 additions and 12 deletions

View file

@ -53,6 +53,8 @@ on:
- 'studio/backend/requirements/**'
- 'studio/frontend/package.json'
- 'studio/frontend/package-lock.json'
- 'studio/backend/core/data_recipe/oxc-validator/package.json'
- 'studio/backend/core/data_recipe/oxc-validator/package-lock.json'
- 'studio/src-tauri/Cargo.toml'
- 'studio/src-tauri/Cargo.lock'
- 'pyproject.toml'
@ -278,7 +280,7 @@ jobs:
{
echo "## Lockfile supply-chain audit"
echo
echo "Scanned: studio/frontend/package-lock.json + studio/src-tauri/Cargo.lock"
echo "Scanned: studio/frontend/package-lock.json + studio/backend/core/data_recipe/oxc-validator/package-lock.json + studio/src-tauri/Cargo.lock"
echo
echo "No structural anomalies or known IOC strings."
} >> "$GITHUB_STEP_SUMMARY"
@ -307,6 +309,22 @@ jobs:
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: npm audit (oxc-validator runtime)
# Same audit surface, separate npm project (oxc-parser, oxlint).
continue-on-error: true
working-directory: studio/backend/core/data_recipe/oxc-validator
run: |
set +e
npm audit --audit-level=high | tee "$GITHUB_WORKSPACE/logs-npm-audit-oxc.txt"
npm audit --json > "$GITHUB_WORKSPACE/logs-npm-audit-oxc.json" || true
{
echo "## npm audit (oxc-validator)"
echo
echo '```'
tail -200 "$GITHUB_WORKSPACE/logs-npm-audit-oxc.txt"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# cargo: Studio Tauri shell
# ─────────────────────────────────────────────────────────────
@ -348,6 +366,7 @@ jobs:
/tmp/osv-scanner --version
/tmp/osv-scanner scan source \
--lockfile=studio/frontend/package-lock.json \
--lockfile=studio/backend/core/data_recipe/oxc-validator/package-lock.json \
--lockfile=studio/src-tauri/Cargo.lock \
--lockfile=requirements.txt:audit-reqs/unsloth-deps.txt \
--lockfile=requirements.txt:audit-reqs/studio.txt \
@ -913,24 +932,39 @@ jobs:
# downloaded tarball, and only fetches from registry.npmjs.org.
# Initially non-blocking so the baseline can settle; drop
# continue-on-error once the baseline is clean for a week.
#
# Two separate npm projects share this scan surface; scan each.
run: |
set -o pipefail
LOG=logs-scan-npm.txt
python3 scripts/scan_npm_packages.py 2>&1 | tee "$LOG"
LOG2=logs-scan-npm-oxc.txt
python3 scripts/scan_npm_packages.py \
--lockfile studio/backend/core/data_recipe/oxc-validator/package-lock.json \
2>&1 | tee "$LOG2"
{
echo "## scan_npm_packages"
echo "## scan_npm_packages (Studio frontend)"
echo
echo '### Findings (tail)'
echo '```'
tail -300 "$LOG"
echo '```'
echo
echo "## scan_npm_packages (oxc-validator)"
echo
echo '### Findings (tail)'
echo '```'
tail -300 "$LOG2"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: scan-npm-packages-log
path: logs-scan-npm.txt
path: |
logs-scan-npm.txt
logs-scan-npm-oxc.txt
retention-days: 30
# ─────────────────────────────────────────────────────────────────────
@ -1103,6 +1137,9 @@ jobs:
BASE_SHA="${{ github.event.pull_request.base.sha }}"
git show "$BASE_SHA:studio/frontend/package-lock.json" \
> /tmp/base-package-lock.json
# OXC lockfile may not exist on the base ref (newly added).
git show "$BASE_SHA:studio/backend/core/data_recipe/oxc-validator/package-lock.json" \
> /tmp/base-oxc-package-lock.json 2>/dev/null || echo '{}' > /tmp/base-oxc-package-lock.json
- name: Diff for newly-added install-script deps
if: github.event_name == 'pull_request'
@ -1110,6 +1147,9 @@ jobs:
python3 scripts/check_new_install_scripts.py \
--base /tmp/base-package-lock.json \
--head studio/frontend/package-lock.json
python3 scripts/check_new_install_scripts.py \
--base /tmp/base-oxc-package-lock.json \
--head studio/backend/core/data_recipe/oxc-validator/package-lock.json
- name: Skip install-script diff (non-PR trigger)
if: github.event_name != 'pull_request'

View file

@ -87,6 +87,7 @@ jobs:
n = z.namelist()
checks = {
"lockfile shipped": any(s.endswith("studio/frontend/package-lock.json") for s in n),
"oxc lockfile shipped": any(s.endswith("oxc-validator/package-lock.json") for s in n),
"frontend dist shipped": any(s.endswith("studio/frontend/dist/index.html") for s in n),
"no node_modules": not any("studio/frontend/node_modules/" in s for s in n),
"no bun.lock": not any(s.endswith("studio/frontend/bun.lock") for s in n),

View file

@ -33,11 +33,11 @@ _restore_gitignores() {
}
trap _restore_gitignores EXIT
# Use bun if available (faster), fall back to npm. Lockfile-strict on
# both paths so a release build can't pull a fresh caret-range patch
# of any transitive from the registry.
# Use bun if a bun.lock is committed (lockfile-strict, fast), else npm ci.
# bun install --frozen-lockfile cannot migrate from package-lock.json, so if
# only the npm lockfile is present we skip bun and go straight to npm ci.
_install_ok=false
if command -v bun &>/dev/null; then
if [ -f bun.lock ] && command -v bun &>/dev/null; then
if bun install --frozen-lockfile; then
_install_ok=true
else

View file

@ -652,7 +652,10 @@ def audit_cargo_lockfile(path: Path) -> list[Finding]:
# ─────────────────────────────────────────────────────────────────────
DEFAULT_NPM_LOCKFILES = ("studio/frontend/package-lock.json",)
DEFAULT_NPM_LOCKFILES = (
"studio/frontend/package-lock.json",
"studio/backend/core/data_recipe/oxc-validator/package-lock.json",
)
DEFAULT_CARGO_LOCKFILES = ("studio/src-tauri/Cargo.lock",)
@ -671,7 +674,8 @@ def main(argv: list[str] | None = None) -> int:
default = None,
help = (
"Path to a package-lock.json (repeatable). "
"Default: studio/frontend/package-lock.json."
"Default: studio/frontend/package-lock.json plus "
"studio/backend/core/data_recipe/oxc-validator/package-lock.json."
),
)
parser.add_argument(

View file

@ -1327,13 +1327,15 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
$ErrorActionPreference = "Continue"
Push-Location $FrontendDir
$UseBun = $null -ne (Get-Command bun -ErrorAction SilentlyContinue)
# Only use bun when a committed bun.lock is present. bun install
# --frozen-lockfile cannot migrate from package-lock.json, so without
# bun.lock the bun path would always fail.
$UseBun = ($null -ne (Get-Command bun -ErrorAction SilentlyContinue)) -and (Test-Path "bun.lock")
# bun's package cache can become corrupt -- packages get stored with only
# 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 fresh caret-range patch can't land via npm registry.
if ($UseBun) {
Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray
$bunExit = Invoke-SetupCommand { bun install --frozen-lockfile }

View file

@ -367,7 +367,11 @@ _try_bun_install() {
}
_bun_install_ok=false
if command -v bun &>/dev/null; then
# bun install --frozen-lockfile cannot migrate from package-lock.json, so we
# only enter the bun path when a committed bun.lock exists. Without it,
# bun would fail every time and the corrupt-cache retry would clear the
# user's bun cache for nothing.
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