Completes the allowScripts rollout: upgrade CI npm to the 11.x line (node 22 bundles 10.x, which predates the gate) with a loud version guard so the flag can never silently degrade into a warning, then run npm ci --strict-allow-scripts. A dependency that introduces install scripts not covered by the committed policy now fails the job with npm's approve-scripts/deny-scripts instructions; the pre-commit sync hook keeps existing pins fresh after bumps.
175 lines
7.6 KiB
YAML
175 lines
7.6 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Frontend PR gate: lockfile freshness, typecheck, build, and a bundle grep
|
|
# that catches the 2026.5.1 chat-history regression at the JS level.
|
|
#
|
|
# biome runs as non-blocking for now: the codebase currently has accumulated
|
|
# ~470 errors and ~1650 warnings against the existing biome config. Surfacing
|
|
# the count in CI lets us drive it down without forcing a fleet-wide cleanup
|
|
# in the same PR. Drop `continue-on-error` once that number is zero.
|
|
|
|
name: Frontend CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'studio/frontend/**'
|
|
- 'scripts/check_frontend_dep_removal.py'
|
|
- 'tests/studio/test_frontend_dep_removal.py'
|
|
- 'scripts/sync_allow_scripts_pins.py'
|
|
- 'tests/studio/test_sync_allow_scripts_pins.py'
|
|
- '.github/workflows/studio-frontend-ci.yml'
|
|
push:
|
|
branches: [main, pip]
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build:
|
|
name: Frontend build + bundle sanity
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
defaults:
|
|
run:
|
|
working-directory: studio/frontend
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# FIXME: drop this step once @assistant-ui/* and assistant-stream
|
|
# leave 0.x -- on 1.x, caret ranges are conventional. Until then,
|
|
# every 0.minor on this surface is a SemVer-major (this is exactly
|
|
# how 2026.5.1 shipped a broken chat runtime: ^0.12.19 quietly
|
|
# resolved to 0.12.28).
|
|
- name: '@assistant-ui must be pinned exactly (no caret/tilde)'
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
set -e
|
|
if grep -nE '"(@assistant-ui/[a-z-]+|assistant-stream)":[[:space:]]*"[\^~]' studio/frontend/package.json; then
|
|
echo "::error file=studio/frontend/package.json::These packages must be pinned to exact versions until they leave 0.x. Drop the leading ^ or ~."
|
|
exit 1
|
|
fi
|
|
echo "All assistant-ui packages are pinned exactly."
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
|
|
# node 22 bundles npm 10.x, which predates allowScripts. Move to the
|
|
# 11.x line and fail loudly if the gate is still missing, so the
|
|
# strict flag below can never silently degrade into a warning.
|
|
- name: Upgrade npm to 11.x (allowScripts enforcement)
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
npm install -g npm@^11 --no-fund --no-audit
|
|
V=$(npm -v)
|
|
case "$V" in
|
|
11.1[6-9].*|11.[2-9][0-9].*|1[2-9].*) echo "npm $V has allowScripts" ;;
|
|
*) echo "::error::npm $V lacks allowScripts (need >=11.16)"; exit 1 ;;
|
|
esac
|
|
|
|
# Run the structural lockfile scan BEFORE npm ci. A compromised
|
|
# tarball runs its `prepare` / `postinstall` during `npm ci`,
|
|
# so any catch has to fire upstream of that. The scanner is
|
|
# pure-Python read-only; safe to call ahead of every install.
|
|
- name: Lockfile supply-chain audit (pre-install scan)
|
|
working-directory: ${{ github.workspace }}
|
|
run: python3 scripts/lockfile_supply_chain_audit.py
|
|
|
|
# Dependency bumps strand the version-pinned allowScripts entries.
|
|
# The paired pre-commit hook auto-fixes PRs; this is the backstop.
|
|
- name: allowScripts pins must match the lockfile
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
python3 tests/studio/test_sync_allow_scripts_pins.py
|
|
python3 scripts/sync_allow_scripts_pins.py --check
|
|
|
|
- name: Lockfile must agree with package.json (npm ci is strict)
|
|
# The vite 8 chain (rolldown, lightningcss, tailwind oxide) ships napi
|
|
# binaries with no install scripts. The only script-bearing deps are
|
|
# covered by `allowScripts` in package.json (npm >=11.16, default in
|
|
# npm 12). The pre-install lockfile audit above stays the first line
|
|
# of defence -- it fires before any tarball can run code.
|
|
# --strict-allow-scripts: any unreviewed install script hard-fails
|
|
# the job; the sync hook keeps the pins fresh after bumps.
|
|
run: npm ci --strict-allow-scripts --no-fund --no-audit
|
|
|
|
- name: npm ci must not have modified the working tree
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
if ! git diff --quiet -- studio/frontend; then
|
|
echo "::error::npm ci modified files; commit the updated lockfile"
|
|
git status -- studio/frontend
|
|
exit 1
|
|
fi
|
|
|
|
# Catch the common foot-gun: a dep dropped from package.json that is
|
|
# still imported somewhere. The script walks the lockfile dep graph
|
|
# from the new top-level deps and only counts top-level node_modules
|
|
# paths as valid resolution targets for bare src/ imports.
|
|
#
|
|
# actions/checkout uses fetch-depth: 1 by default, so the base branch
|
|
# is not available locally. Fetch the single base commit with an
|
|
# explicit refspec so origin/<base> is reliably created (a bare
|
|
# `git fetch origin <ref>` only updates FETCH_HEAD in some configs).
|
|
- name: Dependency removal safety check
|
|
if: github.event_name == 'pull_request'
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
git fetch --no-tags --depth=1 origin \
|
|
"${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
|
|
python3 scripts/check_frontend_dep_removal.py \
|
|
--base "origin/${{ github.base_ref }}" \
|
|
--enumerate-dead
|
|
python3 tests/studio/test_frontend_dep_removal.py
|
|
|
|
- name: Typecheck
|
|
run: npm run typecheck
|
|
|
|
- name: Build
|
|
run: npm run build
|
|
|
|
- name: Built bundle must not contain Studio's unstable_Provider call site
|
|
run: |
|
|
set -e
|
|
JS=$(ls dist/assets/index-*.js | head -1)
|
|
HITS=$(grep -c 'unstable_Provider:' "$JS" || echo 0)
|
|
echo "main bundle: $JS"
|
|
echo "unstable_Provider: hits=$HITS (assistant-ui internals contribute up to 3)"
|
|
if [ "$HITS" -gt 3 ]; then
|
|
echo "::error file=studio/frontend/src/features/chat/runtime-provider.tsx::Studio bundle still passes unstable_Provider through useRemoteThreadListRuntime; this is the 2026.5.1 chat-history regression. Pass adapters directly into useLocalRuntime instead."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Bundle size budget (75 MB)
|
|
run: |
|
|
SIZE=$(du -sb dist | cut -f1)
|
|
BUDGET=$((75 * 1024 * 1024))
|
|
echo "dist size: $SIZE bytes ($((SIZE/1024/1024)) MB), budget: $BUDGET bytes (75 MB)"
|
|
if [ "$SIZE" -gt "$BUDGET" ]; then
|
|
echo "::error::studio/frontend/dist/ exceeded the 75 MB budget. Drop dead deps (e.g. the unused next dep) or split chunks."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Biome (non-blocking until accumulated drift is cleared)
|
|
continue-on-error: true
|
|
run: npm run biome:check
|
|
|
|
- name: Upload built dist
|
|
# Always upload so a green run is reviewable too -- the dist
|
|
# output catches "tests passed but bundle changed unexpectedly"
|
|
# regressions that would be invisible if we only kept artifacts
|
|
# on failure.
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: studio-frontend-dist
|
|
path: studio/frontend/dist
|
|
retention-days: 3
|