blob: 6fd1fbf9638bee254688548f59ba75a87ac3f1c2 [file]
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
changes:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.plan.outputs.code }}
e2e: ${{ steps.plan.outputs.e2e }}
headless: ${{ steps.plan.outputs.headless }}
runtime_sandbox: ${{ steps.plan.outputs.runtime_sandbox }}
script_mode: ${{ steps.plan.outputs.script_mode }}
storage_stress: ${{ steps.plan.outputs.storage_stress }}
unit: ${{ steps.plan.outputs.unit }}
workspaces: ${{ steps.plan.outputs.workspaces }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: plan
name: Select affected test surfaces
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ "$EVENT_NAME" == "push" ]]; then
node scripts/ci-test-plan.mjs --full >> "$GITHUB_OUTPUT"
else
node scripts/ci-test-plan.mjs --base "$BASE_SHA" --head "$HEAD_SHA" >> "$GITHUB_OUTPUT"
fi
typecheck:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run format:check
- run: npm run build
- run: npm run typecheck
# Dead-code / dependency governance. Entry points and reasoned ignores
# live in knip.json; both workspaces must stay at zero findings.
- name: Knip (apps/desktop)
run: npx knip --workspace apps/desktop
- name: Knip (packages/ui)
run: npx knip --workspace packages/ui
# Dead-CSS governance. Lived only under `check:release`, so a regression
# stayed invisible until a release was cut; it was red on main when this
# step was added. Same class of source-scanning guard as knip above.
- name: Dead CSS
run: node scripts/check-dead-css.mjs --check
# Storybook fidelity: every Product/* story must name the real user path
# to the state it renders. Presence is mechanical; truth stays with the
# reviewer. See apps/desktop/stories/FIDELITY.md.
- name: Story annotations
run: node scripts/check-story-annotations.mjs
test:
needs: changes
if: needs.changes.outputs.unit == 'true' || needs.changes.outputs.script_mode != 'none'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
- uses: astral-sh/setup-uv@v6
if: needs.changes.outputs.headless == 'true'
- name: Install pinned Harbor contract runtime
if: needs.changes.outputs.headless == 'true'
run: |
uv tool install "harbor==0.13.2"
uv tool dir --bin >> "$GITHUB_PATH"
- name: Install Linux runtime dependencies
if: needs.changes.outputs.headless == 'true' || needs.changes.outputs.runtime_sandbox == 'true'
run: sudo apt-get update && sudo apt-get install -y ripgrep bubblewrap
# Ubuntu 24.04 hosted runners gate unprivileged user namespaces through
# AppArmor, which otherwise makes bwrap fail while configuring loopback.
- name: Enable bubblewrap user namespaces
if: needs.changes.outputs.runtime_sandbox == 'true'
run: |
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
if [[ -e /proc/sys/kernel/unprivileged_userns_clone ]]; then
sudo sysctl -w kernel.unprivileged_userns_clone=1
fi
- run: npm ci
# build:test skips the renderer bundle (vite); test:dist only consumes
# tsc outputs (dist/main + dist/renderer side-files), never the vite
# bundle. e2e builds its own renderer in a separate job.
- run: npm run build:test
if: needs.changes.outputs.unit == 'true' || needs.changes.outputs.script_mode != 'none'
- name: Linux sandbox smoke
if: needs.changes.outputs.runtime_sandbox == 'true'
env:
MAKA_REQUIRE_LINUX_SANDBOX_SMOKE: '1'
run: npm exec -w @maka/runtime -- node --test dist/__tests__/linux-sandbox-smoke.test.js
- name: Run fast script tests
if: needs.changes.outputs.script_mode == 'fast' || needs.changes.outputs.script_mode == 'full'
run: npm run test:scripts
- name: Run extended script tests
if: needs.changes.outputs.script_mode == 'extended' || needs.changes.outputs.script_mode == 'full'
run: npm run test:scripts:extended
# Workspaces consume the dist built above. Selection includes reverse
# dependencies, while bounded concurrency avoids both the old serial
# critical path and an unbounded process stampede on two-core runners.
- name: Run affected workspace tests
if: needs.changes.outputs.unit == 'true'
env:
HEADLESS_AFFECTED: ${{ needs.changes.outputs.headless }}
STORAGE_STRESS: ${{ needs.changes.outputs.storage_stress }}
WORKSPACES: ${{ needs.changes.outputs.workspaces }}
run: |
if [[ "$HEADLESS_AFFECTED" == "true" ]]; then
export MAKA_REQUIRE_HARBOR_CONTRACT=1
fi
if [[ "$STORAGE_STRESS" == "true" ]]; then
export MAKA_STORAGE_STRESS=1
fi
node scripts/run-workspace-tests-parallel.mjs --concurrency=3 --workspaces="$WORKSPACES"
e2e:
needs: changes
if: needs.changes.outputs.e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
- run: npm ci
# Electron is a GUI app; on a headless Linux runner it needs a virtual
# display. The suite already runs with show:false, but the process still
# requires an X server to start.
- name: Ensure xvfb
run: command -v xvfb-run >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y xvfb; }
- name: E2E
run: xvfb-run -a npm --workspace @maka/desktop run e2e
# Design governance: the CDP alignment auditor walks the e2e-fixture
# fixtures and fails on same-type height mismatches, mixed-type
# centerline drift, or radius-family splits. Reuses the renderer the
# e2e step just built; same xvfb pattern.
- name: Alignment audit
run: xvfb-run -a node scripts/audit-alignment.mjs
# Storybook is where pixel work happens (see stories/FIDELITY.md), but
# nothing verified that it still renders: story typechecking rides along
# in `typecheck`, while the static build and the render/play smoke ran
# only if someone remembered to run them locally.
#
# The E2E suite above drives Electron through `_electron.launch`, which
# uses Electron's own binary — it never downloads a browser. The smoke
# calls `chromium.launch()`, so the Chromium build must be fetched here
# explicitly; without it the step fails on a missing executable.
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Build Storybook
run: npm --workspace @maka/desktop run build-storybook
- name: Storybook smoke
run: npm --workspace @maka/desktop run smoke:storybook