unsloth/.github/workflows/studio-tauri-smoke.yml
Daniel Han 70a090fd26 studio: harden lockfile supply-chain audit followups for #5604
Three regressions surfaced after #5604 landed. All addressed here with
matching pytest coverage.

1. Default mode no longer blocks on `unsupported-lockfile-version`. A
   lockfile downgraded to v1 (or bumped to an unsupported future
   version) silently passes default CI because the auditor's structural
   walk only runs on v2/v3, so `blocked-known-malicious` /
   `known-ioc-string` findings cannot be produced. v1 downgrade is a
   documented supply-chain attack shape; restoring it to BLOCKING_KINDS.

2. `UNSLOTH_LOCKFILE_AUDIT_SKIP` warnings interpolated the raw env var
   value into `:⚠️:` workflow commands without `_gha_escape()`,
   so a value containing `\n::error::...` was emitted as a second
   physical workflow-command line (GH Actions parses workflow commands
   per physical line). Routing both branches through `_gha_escape()`
   collapses any control char onto a single annotation line.

3. The two workflows that consume `studio/package-lock.json` --
   `studio-tauri-smoke.yml` and `release-desktop.yml` -- did not invoke
   `scripts/lockfile_supply_chain_audit.py` before their `npm install`
   steps. Lifecycle scripts in a compromised lockfile would run before
   the audit could refuse the lockfile, defeating the script's
   "pre-install" guarantee. `release-desktop.yml` additionally has
   `contents: write`, so the gap had real publish-attacker blast
   radius. Both workflows now run the audit before any `npm install`
   or `npm ci`.

Tests
-----
- test_unsupported_lockfile_version_blocks_default: v1 lockfile must
  exit 1 in default mode.
- test_blocking_kinds_contains_unsupported_lockfile_version: direct
  module-level pin against future regressions.
- test_skip_env_warning_escapes_workflow_command_injection: both
  skip-env branches must escape `\n` / `%` so no injected physical
  workflow-command line is produced.
- test_audit_runs_before_npm_install_in_consumer_workflows: regex
  ordering check on the `run:` lines in the two consumer workflows so
  a future reorder cannot silently bring back the pre-install gap.

16/16 pytest pass; default-mode audit still rc=0 on real lockfiles.
2026-05-22 13:00:02 +00:00

132 lines
5.2 KiB
YAML

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
# PR-time smoke for the Tauri desktop wrapper. Builds the frontend and the
# Tauri Linux debug binary, with no codesigning. Catches:
# - tauri.conf.json drift
# - src-tauri Cargo.toml or rust source breakage
# - Tauri CLI version drift (we pin 2.10.1, matching release-desktop.yml)
# - frontend output not picked up by Tauri's distDir
#
# Linux-only on a free `ubuntu-latest` runner. Mac and Windows desktop builds
# stay in release-desktop.yml (manual `workflow_dispatch`) because they need
# code-signing secrets and ~30 min of runner time each.
name: Studio Tauri CI
on:
pull_request:
paths:
- 'studio/frontend/**'
- 'studio/src-tauri/**'
# CLI rename / signature change can break Tauri's spawned
# `unsloth studio` -- include unsloth_cli in the trigger set.
- 'unsloth_cli/**'
- '.github/workflows/studio-tauri-smoke.yml'
push:
branches: [main, pip]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
linux-debug-build:
name: Tauri Linux debug build (no codesign)
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Linux native deps for Tauri / WebKit2GTK
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \
librsvg2-dev libxdo-dev libssl-dev patchelf
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable @ 2026-03-27
- uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
with:
workspaces: studio/src-tauri -> target
- name: Lockfile supply-chain audit (pre-install scan)
# Must run BEFORE any `npm install` (including the Tauri CLI
# install below). Lifecycle scripts in a compromised lockfile
# would otherwise execute inside this runner before the audit
# gets a chance to refuse the lockfile.
run: python3 scripts/lockfile_supply_chain_audit.py
- name: Install pinned Tauri CLI (matches release-desktop.yml)
# Lifecycle scripts (esbuild native-binary postinstall, etc.) are
# required for `vite build`. The pre-install lockfile structural
# audit (lockfile_supply_chain_audit.py) above 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 --save-dev --prefix studio @tauri-apps/cli@2.10.1 --no-fund --no-audit
- name: Verify pinned Tauri CLI version
run: |
out="$(npx --prefix studio tauri --version)"
echo "$out"
[ "$out" = "tauri-cli 2.10.1" ] || { echo "::error::expected tauri-cli 2.10.1, got $out"; exit 1; }
- name: Frontend build (npm ci, vite)
working-directory: studio/frontend
# 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 ci --no-fund --no-audit
npm run build
test -f dist/index.html
- name: Tauri debug build (Linux, no bundle, no codesign)
# `--debug` + `--no-bundle` keeps this lean: compiles the Rust crate,
# confirms the frontend dist is wired into Tauri, but skips the AppImage
# / .deb production. Code signing is irrelevant because we never produce
# a distributable artifact.
env:
TAURI_SIGNING_PRIVATE_KEY: ''
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
run: npx --prefix studio tauri build --debug --no-bundle
- name: Inspect produced binary
run: |
BIN=$(find studio/src-tauri/target/debug -maxdepth 1 -type f -executable 2>/dev/null \
| grep -Ev '\.(d|so|dylib|dll)$' \
| grep -Ev '/(deps|build|examples)$' \
| head -1)
echo "binary: $BIN"
if [ -z "$BIN" ]; then
echo "::error::Tauri debug binary not produced"
ls -la studio/src-tauri/target/debug/ || true
exit 1
fi
file "$BIN"
du -h "$BIN"
- name: Upload Tauri debug build
# Always upload so a green run leaves the binary inspectable too.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tauri-debug-build
path: |
studio/src-tauri/target/debug
studio/frontend/dist
retention-days: 3