name: CI # Test gate for every PR and push: changed-file formatting, lint, typecheck across # all workspaces, and the full unit-test suite (engine packages and app tests; the # sheets suite also runs the Rust xlsx-sidecar tests). Packaging/signing is handled # by separate release automation — this workflow never builds installers. on: push: branches: [main, 'dev_*', 'release_*'] pull_request: branches: [main] workflow_dispatch: concurrency: group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: test: runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: npm cache-dependency-path: package-lock.json - name: Setup Rust # SHA-pinned (third-party action); the ref no longer selects the # toolchain, so it is passed explicitly. uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable branch, 2026-07 with: toolchain: stable - name: Cache cargo (xlsx-sidecar) uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: workspaces: apps/sheets/native/xlsx-engine - name: Install dependencies run: npm ci - name: Check dependency licenses run: npm run licenses - name: Check Rust dependency licenses (xlsx-sidecar) uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 with: command: check licenses manifest-path: apps/sheets/native/xlsx-engine/Cargo.toml - name: Check formatting env: FORMAT_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }} # For PRs the checkout is the PR merge commit, so its first parent is the # exact main revision the merge was built against. The event's base.sha can # lag behind a fast-moving main, which pulls unrelated main-side files into # the changed-file set and fails the check on files the PR never touched. run: | if [ "${{ github.event_name }}" = "pull_request" ]; then FORMAT_BASE_REF=$(git rev-parse HEAD^1) fi npm run format:check - name: Check theme colors env: FORMAT_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }} run: | if [ "${{ github.event_name }}" = "pull_request" ]; then FORMAT_BASE_REF=$(git rev-parse HEAD^1) fi npm run check:theme-colors - name: Lint run: npm run lint - name: Typecheck run: npm run typecheck - name: Regenerate test fixtures run: npm run fixtures # Generation is byte-deterministic (zip entry mtimes are pinned in # tests/helpers/build-docx.ts); a diff here means the generator and the # committed fixtures have drifted apart. - name: Check committed fixtures match the generator run: git diff --exit-code -- fixtures/generated - name: Test run: npm test # Preservation gate: a no-op-adjacent edit through the OOXML gateway must # leave every untouched package entry byte-identical (fails closed otherwise). - name: Sheets compatibility gate run: | npm run fixtures -w @hermesoffice/sheets npm run compat -w @hermesoffice/sheets e2e: # 22.04, not -latest: 24.04's AppArmor hardening blocks Electron renderer # spawn under xvfb even with --no-sandbox and the userns sysctl lifted runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: npm cache-dependency-path: package-lock.json - name: Setup Rust uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable branch, 2026-07 with: toolchain: stable - name: Cache cargo (xlsx-sidecar) uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: workspaces: apps/sheets/native/xlsx-engine - name: Install dependencies run: npm ci - name: Build apps run: npm run build:all - name: Generate sheets fixtures run: npm run fixtures -w @hermesoffice/sheets # Newer Ubuntu kernels ship an AppArmor policy that blocks unprivileged # user namespaces, which kills the Chromium zygote even with --no-sandbox # (window opens, renderer never loads). The knob only exists on kernels # that enforce the restriction, so skip it where it is absent instead of # failing the job (22.04 runner images vary by kernel). - name: Allow unprivileged user namespaces (Electron) run: | if sysctl kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 else echo "kernel.apparmor_restrict_unprivileged_userns not present — skipping" fi # Electron needs a display server on Linux runners. DEBUG=pw:browser # forwards the Electron process stdout/stderr into the CI log so launch # failures are diagnosable. - name: E2E (Electron shell) env: DEBUG: 'pw:browser' run: xvfb-run --auto-servernum -- npm run test:e2e - name: Upload E2E artifacts on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: e2e-artifacts path: | e2e/artifacts e2e/test-results retention-days: 7