fix(scroll): keep bottom glued when a reaction grows a mid-viewport row #1148
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main, devel] | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| # Backstop against a hung step (default GitHub timeout is 6h). A healthy run is ~7min. | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build SDK | |
| run: npm run build:sdk | |
| - name: Run tests | |
| run: npm test | |
| - name: Type check | |
| run: npm run typecheck | |
| - name: Lint SDK | |
| run: npm run lint --workspace=@fluux/sdk | |
| - name: Lint App | |
| run: npm run lint --workspace=@xmpp/fluux | |
| - name: Check for unstable Zustand selectors | |
| run: | | |
| # Zustand selectors passed to useStore/useSyncExternalStore MUST return | |
| # referentially stable values. If a selector creates a new object, array, | |
| # Set, or closure on every call, Object.is() comparison fails and React | |
| # enters an infinite re-render loop (error #185: Maximum update depth | |
| # exceeded). | |
| # | |
| # Dangerous patterns detected by this check: | |
| # | |
| # 1. Closure-returning selectors — a new function is created on every call: | |
| # useRoomStore((s) => (jid) => s.rooms.has(jid)) // BAD | |
| # Fix: use imperative store access instead: | |
| # useCallback((jid) => roomStore.getState().rooms.has(jid), []) | |
| # | |
| # 2. Fallback with || [] — creates a new empty array each time: | |
| # useIgnoreStore((s) => s.ignoredUsers[jid] || []) // BAD | |
| # Fix: use a module-level stable reference: | |
| # const EMPTY: T[] = [] | |
| # useIgnoreStore((s) => s.ignoredUsers[jid] ?? EMPTY) | |
| # | |
| # 3. Fallback with new Set()/new Map() — same issue with non-primitives: | |
| # return get().votedPollIds.get(jid) ?? new Set() // BAD | |
| # Fix: const EMPTY_SET = new Set<string>() | |
| # return get().votedPollIds.get(jid) ?? EMPTY_SET | |
| ERRORS=0 | |
| # Pattern 1: Closure-returning selectors (arrow function inside selector) | |
| if grep -rn --include='*.ts' --include='*.tsx' \ | |
| -E 'use\w+Store\(\(s\) => \(' \ | |
| packages/ apps/ \ | |
| | grep -v 'node_modules' | grep -v '.test.' | grep -v '// '; then | |
| echo "::error::Found closure-returning Zustand selector(s). These create a new function on every store update, causing infinite re-render loops." | |
| ERRORS=1 | |
| fi | |
| # Pattern 2: || [] fallback in store selectors | |
| if grep -rn --include='*.ts' --include='*.tsx' \ | |
| -E 'use\w+Store\(\(s\) =>.*\|\| \[\]' \ | |
| packages/ apps/ \ | |
| | grep -v 'node_modules' | grep -v '.test.'; then | |
| echo "::error::Found || [] fallback in Zustand selector(s). Use a stable empty reference (const EMPTY = []) with ?? instead." | |
| ERRORS=1 | |
| fi | |
| # Pattern 3: ?? new Set/Map/Array in store getters used as selectors | |
| if grep -rn --include='*.ts' --include='*.tsx' \ | |
| -E '\?\? new (Set|Map|Array)\(\)' \ | |
| packages/fluux-sdk/src/stores/ \ | |
| | grep -v 'node_modules' | grep -v '.test.'; then | |
| echo "::error::Found ?? new Set/Map/Array() in store code. Use a module-level EMPTY constant instead." | |
| ERRORS=1 | |
| fi | |
| if [ "$ERRORS" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| echo "No unstable Zustand selector patterns found." | |
| e2e-scroll: | |
| name: Scroll invariants (e2e) | |
| runs-on: ubuntu-latest | |
| # Backstop against a hung dev server / browser install (default GitHub timeout is 6h). | |
| # A healthy run is ~7min; the Playwright OS-dep apt install can be slow on an unlucky | |
| # mirror, so this is set generously — it only fires on a genuine hang. | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| # The Playwright webServer runs `npm run dev` (vite), which imports @fluux/sdk. | |
| # The dev build is required because the harness relies on import.meta.env.DEV | |
| # test seams (__fluuxGetVirtOffset / __fluuxTriggerLoadOlder) that are stripped | |
| # from production builds. | |
| - name: Build SDK | |
| run: npm run build:sdk | |
| # Cache the browser binaries (~/.cache/ms-playwright) so a slow CDN can't add minutes to | |
| # the download. Caches created in pull_request runs are scoped to that PR, so the shared | |
| # copy is seeded from main by playwright-cache.yml; restore-keys lets a Playwright bump | |
| # start from the previous cache and download only the changed browsers. | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-playwright- | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: npx playwright install chromium webkit | |
| # The OS-level deps (apt) live on the ephemeral runner, so `install-deps` must run every | |
| # time — but the azure apt mirror can be very slow at certain times of day, so the | |
| # downloaded .deb files are cached and pre-seeded into apt's archive dir. apt validates | |
| # cached debs by hash and re-downloads only what changed, so a stale cache degrades | |
| # gracefully. Keyed on the runner image version (package versions roll with it, ~weekly); | |
| # restore-only here — the shared cache is seeded from main by playwright-cache.yml. | |
| - name: Compute apt package cache key | |
| id: apt-cache-key | |
| run: echo "key=apt-debs-$ImageOS-$ImageVersion" >> "$GITHUB_OUTPUT" | |
| - name: Restore apt package cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/.apt-debs | |
| key: ${{ steps.apt-cache-key.outputs.key }} | |
| restore-keys: | | |
| apt-debs- | |
| - name: Seed apt archives from cache | |
| run: | | |
| if [ -d "$HOME/.apt-debs" ]; then | |
| sudo cp "$HOME"/.apt-debs/*.deb /var/cache/apt/archives/ 2>/dev/null || true | |
| echo "Seeded $(ls "$HOME"/.apt-debs/*.deb 2>/dev/null | wc -l) cached .deb file(s)" | |
| fi | |
| - name: Install Playwright OS dependencies | |
| run: npx playwright install-deps chromium webkit | |
| # CI=true is set by GitHub Actions → the config enables retries(2), forbidOnly, | |
| # and the github + html reporters. Blocks the PR if any invariant regresses. | |
| - name: Run scroll invariants (chromium + webkit) | |
| run: npm run test:scroll | |
| - name: Upload Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-scroll-report | |
| path: playwright-report/ | |
| retention-days: 7 | |
| rust: | |
| name: Rust | |
| runs-on: ubuntu-latest | |
| # Backstop against a hung build/test (default GitHub timeout is 6h). A cached run is ~5min. | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| libxss-dev \ | |
| libdbus-1-dev | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: apps/fluux/src-tauri | |
| - name: Run tests | |
| working-directory: apps/fluux/src-tauri | |
| run: cargo test --locked | |
| - name: Clippy | |
| working-directory: apps/fluux/src-tauri | |
| run: cargo clippy --locked -- -D warnings | |
| - name: Check generated files are up to date | |
| run: git diff --exit-code apps/fluux/src-tauri/gen/ |