Skip to content

What a first-run user sees on launch is now pinned by tests instead o… #430

What a first-run user sees on launch is now pinned by tests instead o…

What a first-run user sees on launch is now pinned by tests instead o… #430

Workflow file for this run

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"