What a first-run user sees on launch is now pinned by tests instead o… #430
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: | |
| workflow_dispatch: | |
| inputs: | |
| run_all: | |
| description: 'Run all checks (bypass change detection)' | |
| type: boolean | |
| default: true | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| jobs: | |
| # =========================================== | |
| # Determine which jobs to run based on changed files | |
| # =========================================== | |
| changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| rust: ${{ steps.filter.outputs.rust }} | |
| svelte: ${{ steps.filter.outputs.svelte }} | |
| desktop: ${{ steps.filter.outputs.desktop }} | |
| website: ${{ steps.filter.outputs.website }} | |
| api-server: ${{ steps.filter.outputs.api-server }} | |
| dashboard: ${{ steps.filter.outputs.dashboard }} | |
| scripts: ${{ steps.filter.outputs.scripts }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| # Filter design rules (the `ci-coverage` check validates the non-glob paths exist): | |
| # - A filter must cover every path its job's checks read, not just the obvious app dir. | |
| # Example: `rust` includes `test/smb-servers/` because the SMB integration tests run | |
| # against those container configs; `svelte` includes `eslint-plugins/` and | |
| # `test/e2e-shared/` because Vitest and ESLint cover them (see vitest.config.ts). | |
| # - `.mise.toml` (toolchain versions) and this workflow file are in every filter: | |
| # changing either can change any job's behavior, so everything reruns. | |
| # - `pnpm-lock.yaml` is in every Node-based filter so lockfile-only bumps | |
| # (pnpm dedupe, transitive updates) still run the affected apps' checks. | |
| - name: Detect file changes | |
| uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: filter | |
| with: | |
| filters: | | |
| rust: | |
| - 'apps/desktop/src-tauri/**' | |
| - 'apps/desktop/test/smb-servers/**' | |
| - 'crates/**' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'rust-toolchain.toml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| svelte: | |
| - 'apps/desktop/src/**' | |
| - 'apps/desktop/static/**' | |
| - 'apps/desktop/test/e2e-shared/**' | |
| - 'apps/desktop/eslint-plugins/**' | |
| - 'apps/desktop/scripts/**' | |
| - 'apps/desktop/package.json' | |
| - 'apps/desktop/svelte.config.js' | |
| - 'apps/desktop/vite.config.js' | |
| - 'apps/desktop/vitest.config.ts' | |
| - 'apps/desktop/eslint.config.js' | |
| - 'apps/desktop/tsconfig.json' | |
| - 'pnpm-lock.yaml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| desktop: | |
| # Gates the Linux E2E job, which builds the full app: frontend + the whole | |
| # Rust workspace, so the Rust workspace inputs belong here too. | |
| - 'apps/desktop/**' | |
| - 'crates/**' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'rust-toolchain.toml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| website: | |
| - 'apps/website/**' | |
| - '.dockerignore' | |
| - 'CHANGELOG.md' | |
| - 'pnpm-lock.yaml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| api-server: | |
| - 'apps/api-server/**' | |
| - 'pnpm-lock.yaml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| dashboard: | |
| - 'apps/analytics-dashboard/**' | |
| - 'pnpm-lock.yaml' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| scripts: | |
| # `apps/desktop/scripts/` holds Go files the scripts-go-* checks scan | |
| # (see GetGoDirectories in scripts/check/checks/common.go). | |
| - 'scripts/**' | |
| - 'apps/desktop/scripts/**' | |
| - '.mise.toml' | |
| - '.github/workflows/ci.yml' | |
| # =========================================== | |
| # Desktop app - Rust backend | |
| # =========================================== | |
| desktop-rust: | |
| name: Desktop (Rust) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.rust == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| # Reclaim ~15-20 GB of preinstalled SDKs we never use. The runner ships | |
| # ~14 GB free; once rust-cache restores ~1.7 GB of `target/` + the cargo | |
| # registry/index AND the SMB integration tests build their own archives | |
| # while Docker holds the SMB container images, a cold-passing build runs | |
| # out of disk ("No space left on device" linking libcmdr_lib.a). Freeing | |
| # the Android SDK alone (~9 GB) is the bulk; the rest is headroom. Best- | |
| # effort (`|| true`): dir names drift across runner images. `df -h` lands | |
| # in the log so a future squeeze is diagnosable at a glance. | |
| - name: Free disk space | |
| run: | | |
| sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/share/swift /opt/hostedtoolcache/CodeQL || true | |
| df -h / | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| # Caches the compiled `target/` dir plus the cargo registry/index/git deps, | |
| # keyed on Cargo.lock + rustc. The old registry-only cache left the full | |
| # ~1000-crate Tauri tree to recompile cold every run (~16 min); with target/ | |
| # cached, an unchanged-deps push recompiles only the `cmdr` crate. rust-cache | |
| # prunes intelligently to stay under GitHub's 10 GB per-repo cache ceiling. | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| - name: Install Tauri dependencies (Linux) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libacl1-dev | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Check rustfmt | |
| run: ./scripts/check/check --check desktop-rust-rustfmt --ci | |
| # Cheap static lints first, so a violation fails before the long compiles below. | |
| - name: Check cfg-gate | |
| run: ./scripts/check/check --check desktop-rust-cfg-gate --ci | |
| - name: Check log-error-macro | |
| run: ./scripts/check/check --check desktop-rust-log-error-macro --ci | |
| - name: Check error-string-match | |
| run: ./scripts/check/check --check desktop-rust-error-string-match --ci | |
| - name: Check lock-poison | |
| run: ./scripts/check/check --check desktop-rust-lock-poison --ci | |
| - name: Check test-sleep | |
| run: ./scripts/check/check --check desktop-rust-test-sleep --ci | |
| - name: Check mtp-dropping-timeout | |
| run: ./scripts/check/check --check desktop-rust-mtp-dropping-timeout --ci | |
| - name: Check mtp-no-transport-reset | |
| run: ./scripts/check/check --check desktop-rust-mtp-no-transport-reset --ci | |
| - name: Check ipc-enum-camelcase | |
| run: ./scripts/check/check --check desktop-rust-ipc-enum-camelcase --ci | |
| - name: Check pluralize-noun | |
| run: ./scripts/check/check --check desktop-pluralize-noun --ci | |
| - name: Run jscpd | |
| run: ./scripts/check/check --check desktop-rust-jscpd --ci | |
| - name: Run cargo-machete | |
| run: ./scripts/check/check --check desktop-rust-cargo-machete --ci | |
| # Fails if THIRD-PARTY-NOTICES.md doesn't match the current lockfiles. | |
| # Lives in this job for the cargo toolchain plus rust-cache, which keeps | |
| # the `cargo install cargo-about` build out of every subsequent run. | |
| - name: Check third-party notices | |
| run: ./scripts/check/check --check desktop-third-party-notices --ci | |
| - name: Run clippy | |
| run: ./scripts/check/check --check desktop-rust-clippy --ci | |
| - name: Run Rust tests | |
| run: ./scripts/check/check --check desktop-rust-tests --ci | |
| - name: Run SMB integration tests | |
| run: ./scripts/check/check --check desktop-rust-integration-tests --ci | |
| # =========================================== | |
| # Desktop app - Svelte frontend | |
| # =========================================== | |
| desktop-svelte: | |
| name: Desktop (Svelte) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.svelte == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Cache pnpm | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.pnpm-store | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate SvelteKit types | |
| run: pnpm exec svelte-kit sync | |
| working-directory: ./apps/desktop | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Run ESLint | |
| run: ./scripts/check/check --check desktop-svelte-eslint --ci | |
| - name: Run Stylelint | |
| run: ./scripts/check/check --check desktop-svelte-stylelint --ci | |
| - name: Check unused CSS | |
| run: ./scripts/check/check --check desktop-svelte-css-unused --ci | |
| - name: Check a11y contrast | |
| run: ./scripts/check/check --check desktop-svelte-a11y-contrast --ci | |
| - name: Check a11y coverage | |
| run: ./scripts/check/check --check desktop-svelte-a11y-coverage --ci | |
| - name: Check UI primitive coverage | |
| run: ./scripts/check/check --check desktop-svelte-ui-primitive-coverage --ci | |
| - name: Check dialog gallery coverage | |
| run: ./scripts/check/check --check desktop-svelte-dialog-gallery-coverage --ci | |
| - name: Check btn-restyle | |
| run: ./scripts/check/check --check desktop-svelte-btn-restyle --ci | |
| - name: Check bare polls | |
| run: ./scripts/check/check --check desktop-svelte-bare-poll --ci | |
| - name: Run svelte-check | |
| run: ./scripts/check/check --check desktop-svelte-check --ci | |
| - name: Check import cycles | |
| run: ./scripts/check/check --check desktop-svelte-import-cycles --ci | |
| - name: Check message-keys freshness | |
| run: ./scripts/check/check --check desktop-message-keys-fresh --ci | |
| - name: Check message-key naming | |
| run: ./scripts/check/check --check desktop-message-key-naming --ci | |
| - name: Check for unused message keys | |
| run: ./scripts/check/check --check desktop-message-keys-unused --ci | |
| - name: Check i18n placeholder/tag parity | |
| run: ./scripts/check/check --check desktop-i18n-parity --ci | |
| - name: Check i18n ICU validity | |
| run: ./scripts/check/check --check desktop-i18n-icu --ci | |
| - name: Check i18n tag/param name collisions | |
| run: ./scripts/check/check --check desktop-i18n-tag-param-collision --ci | |
| - name: Check <Trans> snippet parity | |
| run: ./scripts/check/check --check desktop-i18n-trans-snippet-parity --ci | |
| - name: Check i18n plural-category coverage | |
| run: ./scripts/check/check --check desktop-i18n-plural --ci | |
| - name: Check i18n translation coverage | |
| run: ./scripts/check/check --check desktop-i18n-coverage --ci | |
| - name: Run Knip | |
| run: ./scripts/check/check --check desktop-svelte-knip --ci | |
| - name: Check type drift | |
| run: ./scripts/check/check --check desktop-svelte-type-drift --ci | |
| - name: Typecheck Linux E2E suite | |
| run: ./scripts/check/check --check desktop-svelte-e2e-linux-typecheck --ci | |
| - name: Run Svelte tests | |
| run: ./scripts/check/check --check desktop-svelte-tests --ci | |
| # =========================================== | |
| # Desktop app - Playwright E2E on Linux | |
| # =========================================== | |
| desktop-e2e-linux: | |
| name: E2E tests (Playwright, Linux) | |
| runs-on: ubuntu-latest | |
| # Gated ONLY on change detection — not on desktop-rust OR desktop-svelte: | |
| # - desktop-rust: independent. A Rust unit-test failure shouldn't skip e2e | |
| # (and vice versa). Gating them together hid a 4-day red-main streak in | |
| # May 2026 where a flaky accent-color unit test blocked e2e on PRs that | |
| # touched neither path. | |
| # - desktop-svelte: dropped (June 2026) to cut the critical path. e2e's | |
| # Docker build runs its OWN frontend build (beforeBuildCommand), so it | |
| # never consumed desktop-svelte's artifacts — the gate was only a "don't | |
| # spend e2e compute if svelte's quality checks are red" optimization, and | |
| # it forced e2e to wait ~4 min for desktop-svelte before starting. Now e2e | |
| # starts at t=0, the single biggest wall-clock win on the per-push path. | |
| # Trade: e2e runs even when svelte-check / eslint / svelte tests fail | |
| # (rare); a genuine frontend BUILD error still fails e2e's own build. | |
| # `changes` always runs, so no always()/skip handling is needed here. | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.desktop == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| # The runner ships ~14 GB free; this job holds the ~3.5 GB E2E base image | |
| # AND (on a base-cache miss) its docker-save tar at the same time, on top | |
| # of the cargo/target bind-mount caches and the SMB container images. | |
| # Reclaim unused preinstalled SDKs up front, same as desktop-rust. | |
| # Best-effort (`|| true`): dir names drift across runner images. | |
| - name: Free disk space | |
| run: | | |
| sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/share/swift /opt/hostedtoolcache/CodeQL || true | |
| df -h / | |
| - name: Prepare Docker cache dirs | |
| run: mkdir -p /tmp/cmdr-docker-cache/cargo /tmp/cmdr-docker-cache/target /tmp/cmdr-docker-base-image | |
| - name: Cache Cargo + target (via host bind mounts into Docker) | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: | | |
| /tmp/cmdr-docker-cache/cargo | |
| /tmp/cmdr-docker-cache/target | |
| key: ${{ runner.os }}-docker-e2e-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-docker-e2e- | |
| # The E2E base image (apt packages, Node, Rust — see Dockerfile.base) as | |
| # a docker-save tar, so each run skips the cold ~4-min image build. | |
| # Keyed on the Dockerfile.base content, matching e2e-linux.sh's own | |
| # content-hash tag. Deliberately NO restore-keys: a stale tar's image | |
| # carries a different content-hash tag, so the script would rebuild | |
| # anyway — restoring multi-GB of unusable tar only wastes disk and time. | |
| - name: Cache E2E Docker base image | |
| id: base-image-cache | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: /tmp/cmdr-docker-base-image | |
| key: ${{ runner.os }}-e2e-base-image-${{ hashFiles('apps/desktop/test/e2e-linux/docker/Dockerfile.base') }} | |
| - name: Load E2E base image from cache | |
| if: steps.base-image-cache.outputs.cache-hit == 'true' | |
| run: | | |
| docker load -i /tmp/cmdr-docker-base-image/base.tar | |
| # Reclaim ~3.5 GB right away: with no restore-keys, a hit is always | |
| # exact, so the cache post-step won't save and the tar is dead weight. | |
| rm /tmp/cmdr-docker-base-image/base.tar | |
| - name: Run E2E tests in Docker | |
| run: ./scripts/e2e-linux.sh | |
| working-directory: ./apps/desktop | |
| env: | |
| # Point Docker volumes at host paths so actions/cache can persist them | |
| CARGO_VOLUME: /tmp/cmdr-docker-cache/cargo | |
| TARGET_VOLUME: /tmp/cmdr-docker-cache/target | |
| # On a base-cache miss, export the freshly built base image so the | |
| # actions/cache post-step (which runs even when tests fail) can save it. | |
| # `docker save cmdr-e2e-base` packs every tag of the repo (the content | |
| # hash + latest), so the next run's `docker load` restores the exact tag | |
| # e2e-linux.sh looks for. Guarded on the image existing in case the run | |
| # died before the base build. | |
| - name: Export E2E base image for the cache save | |
| if: always() && steps.base-image-cache.outputs.cache-hit != 'true' | |
| run: | | |
| if [ -n "$(docker image ls -q cmdr-e2e-base)" ]; then | |
| docker save cmdr-e2e-base -o /tmp/cmdr-docker-base-image/base.tar | |
| fi | |
| # The Docker build runs as root, so it writes the bind-mounted cache dirs | |
| # as root. The actions/cache post-step then runs as the runner user and | |
| # `tar` can't read root-owned files — so the save failed (warning only, | |
| # never fatal) on EVERY run since this cache was added, and the next run | |
| # always restored nothing → a fully cold ~10-min Tauri build every time. | |
| # Hand ownership back so the post-step can pack the cache. `always()` so | |
| # the build cache still saves when a test fails (the build succeeded). | |
| - name: Reclaim cache dir ownership for the save step | |
| if: always() | |
| run: sudo chown -R "$(id -u):$(id -g)" /tmp/cmdr-docker-cache | |
| - name: Upload E2E screenshots on failure | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: failure() | |
| with: | |
| name: e2e-screenshots | |
| path: apps/desktop/test-results/ | |
| retention-days: 7 | |
| # =========================================== | |
| # Website | |
| # =========================================== | |
| website: | |
| name: Website | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.website == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Cache pnpm | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.pnpm-store | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Run ESLint | |
| run: ./scripts/check/check --check website-eslint --ci | |
| - name: Run typecheck | |
| run: ./scripts/check/check --check website-typecheck --ci | |
| - name: Build website | |
| run: ./scripts/check/check --check website-build --ci | |
| - name: Run html-validate | |
| run: ./scripts/check/check --check website-html-validate --ci | |
| - name: Check analytics injection | |
| run: ./scripts/check/check --check website-analytics-injection --ci | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| working-directory: ./apps/website | |
| - name: Run E2E tests | |
| run: ./scripts/check/check --check website-e2e --ci | |
| - name: Run Lighthouse CI | |
| run: pnpm test:lighthouse | |
| working-directory: ./apps/website | |
| env: | |
| LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} | |
| - name: Upload Lighthouse report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: always() | |
| with: | |
| name: lighthouse-report | |
| path: apps/website/.lighthouseci/ | |
| retention-days: 7 | |
| # =========================================== | |
| # API server | |
| # =========================================== | |
| api-server: | |
| name: API server | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.api-server == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Cache pnpm | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.pnpm-store | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Run ESLint | |
| run: ./scripts/check/check --check api-server-eslint --ci | |
| - name: Run typecheck | |
| run: ./scripts/check/check --check api-server-typecheck --ci | |
| - name: Run tests | |
| run: ./scripts/check/check --check api-server-tests --ci | |
| # =========================================== | |
| # Analytics dashboard | |
| # =========================================== | |
| # The dashboard has no entries in the check registry (it's a small private | |
| # SvelteKit app), so this job runs its package scripts directly. Without it, | |
| # dashboard changes only got validated by the deploy build, after landing on main. | |
| dashboard: | |
| name: Analytics dashboard | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.dashboard == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Cache pnpm | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.pnpm-store | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run svelte-check | |
| run: pnpm check | |
| working-directory: ./apps/analytics-dashboard | |
| - name: Run tests | |
| run: pnpm test | |
| working-directory: ./apps/analytics-dashboard | |
| - name: Build | |
| run: pnpm build | |
| working-directory: ./apps/analytics-dashboard | |
| # =========================================== | |
| # Repo hygiene (always runs, no change gate) | |
| # =========================================== | |
| # Catches what per-app filters can't: oxfmt formats the whole monorepo (docs, | |
| # configs, workflows), so a docs-only commit must still run it — otherwise | |
| # unformatted markdown lands on main and fails the next unrelated PR. Also the | |
| # single home of changelog-commit-links (it was duplicated across three jobs) | |
| # and the workflows/registry guards, which watch files no app filter covers. | |
| hygiene: | |
| name: Repo hygiene | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 # full history so changelog-commit-links can resolve historical SHAs | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Cache pnpm | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.pnpm-store | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Check oxfmt | |
| run: ./scripts/check/check --check oxfmt --ci | |
| - name: Check CHANGELOG commit links | |
| run: ./scripts/check/check --check changelog-commit-links --ci | |
| - name: Check docs reachable from AGENTS.md | |
| run: ./scripts/check/check --check docs-reachable --ci | |
| - name: Check no dead links in docs | |
| run: ./scripts/check/check --check docs-dead-links --ci | |
| - name: Check § pointers name real headings | |
| run: ./scripts/check/check --check docs-section-refs --ci | |
| - name: Check agent-doc table hygiene | |
| run: ./scripts/check/check --check docs-table-hygiene --ci | |
| - name: Check no path-shaped link text in docs | |
| run: ./scripts/check/check --check docs-link-text --ci | |
| - name: Check every CLAUDE.md has a sibling DETAILS.md | |
| run: ./scripts/check/check --check claude-md-details-sibling --ci | |
| - name: Check workflow hardening | |
| run: ./scripts/check/check --check workflows-hardening --ci | |
| - name: Check workflow rustup pins | |
| run: ./scripts/check/check --check workflows-rustup --ci | |
| - name: Check CI coverage of registry checks | |
| run: ./scripts/check/check --check ci-coverage --ci | |
| # =========================================== | |
| # Scripts (Go checks) | |
| # =========================================== | |
| scripts: | |
| name: Scripts (Go) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.run_all || needs.changes.outputs.scripts == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 | |
| - name: Install tools with mise | |
| run: mise install | |
| - name: Build check tool | |
| run: go build -o check . | |
| working-directory: ./scripts/check | |
| - name: Check gofmt | |
| run: ./scripts/check/check --check scripts-go-gofmt --ci | |
| - name: Run go vet | |
| run: ./scripts/check/check --check scripts-go-vet --ci | |
| - name: Run staticcheck | |
| run: ./scripts/check/check --check scripts-go-staticcheck --ci | |
| - name: Run ineffassign | |
| run: ./scripts/check/check --check scripts-go-ineffassign --ci | |
| - name: Run misspell | |
| run: ./scripts/check/check --check scripts-go-misspell --ci | |
| - name: Run gocyclo | |
| run: ./scripts/check/check --check scripts-go-gocyclo --ci | |
| - name: Run nilaway | |
| run: ./scripts/check/check --check scripts-go-nilaway --ci | |
| - name: Run deadcode | |
| run: ./scripts/check/check --check scripts-go-deadcode --ci | |
| - name: Run Go tests | |
| run: ./scripts/check/check --check scripts-go-tests --ci | |
| # =========================================== | |
| # Website deployment | |
| # =========================================== | |
| # This is the ONLY website deploy path. There used to be a standalone | |
| # deploy-website.yml that fired on the same paths — that deployed twice per | |
| # push and, worse, deployed even when checks failed (it didn't wait for the | |
| # website job). Keep deployment here, gated on the website checks passing. | |
| # `workflow_dispatch` on main (with run_all) doubles as the manual deploy lever. | |
| deploy-website: | |
| name: Deploy website | |
| runs-on: ubuntu-latest | |
| needs: website | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| steps: | |
| - name: Trigger deploy webhook | |
| run: | | |
| # Compute HMAC-SHA256 signature | |
| PAYLOAD='{}' | |
| SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.DEPLOY_WEBHOOK_SECRET }}" | cut -d' ' -f2) | |
| # Send signed request to webhook | |
| curl -f -X POST https://getcmdr.com/hooks/deploy-website \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Hub-Signature-256: sha256=$SIGNATURE" \ | |
| -d "$PAYLOAD" | |
| # =========================================== | |
| # Summary job for branch protection | |
| # =========================================== | |
| ci-ok: | |
| name: CI OK | |
| runs-on: ubuntu-latest | |
| needs: [desktop-rust, desktop-svelte, desktop-e2e-linux, website, api-server, dashboard, scripts, hygiene] | |
| if: always() | |
| steps: | |
| - name: Check all jobs passed | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then | |
| echo "Some jobs failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "Some jobs were cancelled" | |
| exit 1 | |
| fi | |
| echo "All jobs passed or were skipped" |