PR #5604 committed three npm lockfiles (studio/, studio/frontend/, studio/backend/core/data_recipe/oxc-validator/) but the install paths still ran `npm install` -- mutates the lockfile, ignores integrity hashes. Flip them to `npm ci`, which is non-mutating and refuses to run unless the committed package-lock.json matches the dependency tree byte-for-byte. This closes the gap between "the lockfile passes the audit" and "the install respected that lockfile". For bun: gate every `bun install` on `[ -f bun.lock ]` (Test-Path on Windows) plus `command -v bun` (Get-Command), and add `--frozen-lockfile --no-progress` to the bun invocation. No bun.lock is committed in this PR, so the gate fails closed today and execution falls through to `npm ci`. When the eventual bun.lock PR lands, the gate auto-activates with no further script edits. Existing bun infrastructure (cache-corruption retry + critical-binary verification + npm fallback) is kept intact so the auto-upgrade is clean. Deliberately NOT introducing _BUN_PIN_VERSION or the 3-attempt cache-corruption recovery ladder; both require committed bun.lock to be useful. Touches three files only: - build.sh - studio/setup.sh - studio/setup.ps1
112 lines
3.8 KiB
Bash
112 lines
3.8 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# PyPI/Studio release publishing must use `./build.sh publish` (or an
|
|
# equivalent stamp -> build -> verify-dist -> upload flow) so packaged Studio
|
|
# artifacts include the display-only Studio release version.
|
|
|
|
# 1. Build frontend (Vite outputs to dist/)
|
|
cd studio/frontend
|
|
|
|
# Clean stale dist to force a full rebuild
|
|
rm -rf dist
|
|
|
|
# Tailwind v4's oxide scanner respects .gitignore in parent directories.
|
|
# Python venvs create a .gitignore with "*" (ignore everything), which
|
|
# prevents Tailwind from scanning .tsx source files for class names.
|
|
# Temporarily hide any such .gitignore during the build, then restore it.
|
|
_HIDDEN_GITIGNORES=()
|
|
_dir="$(pwd)"
|
|
while [ "$_dir" != "/" ]; do
|
|
_dir="$(dirname "$_dir")"
|
|
if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then
|
|
mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild"
|
|
_HIDDEN_GITIGNORES+=("$_dir/.gitignore")
|
|
fi
|
|
done
|
|
|
|
_restore_gitignores() {
|
|
for _gi in "${_HIDDEN_GITIGNORES[@]+"${_HIDDEN_GITIGNORES[@]}"}"; do
|
|
mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true
|
|
done
|
|
}
|
|
trap _restore_gitignores EXIT
|
|
|
|
# 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 [ -f bun.lock ] && command -v bun &>/dev/null; then
|
|
if bun install --frozen-lockfile --no-progress; then
|
|
_install_ok=true
|
|
else
|
|
echo "⚠ bun install --frozen-lockfile failed, falling back to npm ci"
|
|
rm -rf node_modules
|
|
fi
|
|
fi
|
|
if [ "$_install_ok" != "true" ]; then
|
|
if ! npm ci --no-fund --no-audit; then
|
|
echo "❌ ERROR: package install failed" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
npm run build # outputs to studio/frontend/dist/
|
|
|
|
_restore_gitignores
|
|
trap - EXIT
|
|
|
|
# Validate CSS output -- catch truncated Tailwind builds before packaging
|
|
MAX_CSS_SIZE=$(find dist/assets -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}')
|
|
if [ -z "$MAX_CSS_SIZE" ]; then
|
|
echo "❌ ERROR: No CSS files were emitted into dist/assets."
|
|
echo " The frontend build may have failed silently."
|
|
exit 1
|
|
fi
|
|
if [ "$MAX_CSS_SIZE" -lt 100000 ]; then
|
|
echo "❌ ERROR: Largest CSS file is only $((MAX_CSS_SIZE / 1024))KB (expected >100KB)."
|
|
echo " Tailwind may not have scanned all source files."
|
|
echo " Check for .gitignore files blocking the Tailwind oxide scanner."
|
|
exit 1
|
|
fi
|
|
echo "✅ Frontend CSS validated (${MAX_CSS_SIZE} bytes)"
|
|
|
|
cd ../..
|
|
|
|
# 2. Clean old artifacts
|
|
rm -rf build dist *.egg-info
|
|
|
|
# 3. Stamp display-only Studio release metadata for packaged builds.
|
|
_STUDIO_BUILD_INFO="studio/backend/utils/_studio_release_build.py"
|
|
_STUDIO_BUILD_INFO_BACKUP="$(mktemp)"
|
|
cp "$_STUDIO_BUILD_INFO" "$_STUDIO_BUILD_INFO_BACKUP"
|
|
_restore_studio_build_info() {
|
|
cp "$_STUDIO_BUILD_INFO_BACKUP" "$_STUDIO_BUILD_INFO" 2>/dev/null || true
|
|
rm -f "$_STUDIO_BUILD_INFO_BACKUP"
|
|
}
|
|
trap _restore_studio_build_info EXIT
|
|
|
|
if [ "${1:-}" = "publish" ]; then
|
|
STUDIO_STAMPED_VERSION="$(python scripts/stamp_studio_release.py --require-release)"
|
|
else
|
|
STUDIO_STAMPED_VERSION="$(python scripts/stamp_studio_release.py)"
|
|
fi
|
|
|
|
# 4. Build wheel/sdist
|
|
python -m build
|
|
|
|
if [ "${1:-}" = "publish" ]; then
|
|
python scripts/stamp_studio_release.py --verify-dist dist --expected "$STUDIO_STAMPED_VERSION"
|
|
fi
|
|
|
|
_restore_studio_build_info
|
|
trap - EXIT
|
|
|
|
# 5. Optionally publish
|
|
if [ "${1:-}" = "publish" ]; then
|
|
python -m twine upload dist/*
|
|
fi
|