name: ci on: push: branches: - main - 'linear/**' pull_request: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 jobs: rust: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install tmux + build deps run: | apt-get update apt-get install -y --no-install-recommends \ tmux git curl ca-certificates build-essential pkg-config - name: Install rustup + stable toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --default-toolchain stable \ --component rustfmt --component clippy echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # `crates/maestro-web/build.rs` shells out to pnpm whenever # MAESTRO_WEB_DIST is unset, so the Rust build needs node here too. - uses: actions/setup-node@v4 with: node-version: '20' - name: Enable pnpm run: | corepack enable corepack prepare pnpm@9.15.4 --activate # Caching all of `target/` under a prefix restore-key let every stale # artifact a previous run dragged in survive forever, so the entry grew # without bound and cost ~60s to move. rust-cache keys on the rustc # version plus the lockfile and prunes target/ to what that lockfile # actually needs. Only main writes the cache; PRs restore from it and # skip the save, which also keeps concurrent PRs from thrashing it. - name: Cache cargo uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} # nextest runs test *binaries* in parallel, which plain `cargo test` # does not — this suite is 20 integration binaries that mostly sit # waiting on tmux and git. Thread count is capped in # `.config/nextest.toml`; see the comment there. - name: Install cargo-nextest run: | curl -sSfL https://get.nexte.st/latest/linux \ | tar zxf - -C "$HOME/.cargo/bin" - name: fmt run: cargo fmt --all -- --check - name: clippy run: cargo clippy --all-targets -- -D warnings - name: test run: cargo nextest run --all --profile ci # NAP-271: the umbrella `maestro` binary is feature-gated (cli / daemon / # tui). Verify each subset compiles and its tests pass on its own, and # that building with no features at all is rejected. - name: feature-gate matrix run: | set -euo pipefail for f in cli daemon tui; do echo "::group::maestro --no-default-features --features $f" cargo build -p maestro --no-default-features --features "$f" cargo nextest run -p maestro --no-default-features --features "$f" \ --profile ci echo "::endgroup::" done echo "::group::maestro with no features must fail to compile" if cargo build -p maestro --no-default-features 2>/dev/null; then echo "expected build with no features to fail, but it succeeded" >&2 exit 1 fi echo "::endgroup::" web: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Enable pnpm run: | corepack enable corepack prepare pnpm@9.15.4 --activate - name: Install deps run: pnpm -C web install --frozen-lockfile - name: typecheck run: pnpm -C web typecheck - name: test run: pnpm -C web test - name: build run: pnpm -C web build # The flake pins pnpmDeps.hash against web/pnpm-lock.yaml (#98). A lockfile # change invalidates the pin silently elsewhere, so the packaged build has to # fail here too. flake: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # A cold /nix means every run recompiles the entire release closure — # every third-party crate under `lto = "thin"` + `codegen-units = 1`, # which was ~8.5 minutes and the whole pipeline's critical path. With the # store restored, crane's deps-only derivation is already built and only # maestro's own crates recompile. # # The key is the lockfiles that closure derives from, not the sources: a # source-only push is an exact hit, which makes actions/cache skip the # (multi-GB) save. Only the cheap final crates rebuild in that case. - name: Restore nix store id: nix-cache uses: actions/cache/restore@v4 with: path: /nix key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'web/pnpm-lock.yaml', '**/*.nix') }} restore-keys: | nix-${{ runner.os }}- # The runner container is root with no `nixbld` group, but the installer # writes `build-users-group = nixbld` anyway and then fails on it, so the # config goes in first. Flakes are off by default and `nix build .#…` # needs them. Written again afterwards in case the installer replaced it. # # A restored /nix already holds nix itself — the single-user installer # puts its profile under /nix/var/nix/profiles/per-user/root, which is # inside the cached path. Only $HOME (the ~/.nix-profile symlink, the # PATH entry, NIX_SSL_CERT_FILE) lives outside it and has to be redone. - name: Install nix run: | set -euo pipefail mkdir -p /etc/nix printf 'build-users-group =\nexperimental-features = nix-command flakes\n' > /etc/nix/nix.conf profile=/nix/var/nix/profiles/per-user/root/profile if [ -x "$profile/bin/nix" ]; then ln -sfn "$profile" "$HOME/.nix-profile" else curl -fsSL https://nixos.org/nix/install | sh -s -- --no-daemon --yes printf 'build-users-group =\nexperimental-features = nix-command flakes\n' > /etc/nix/nix.conf fi echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH" # Sourcing the installer's profile script is what normally sets this; # without it a restored store cannot talk to cache.nixos.org. if [ -z "${NIX_SSL_CERT_FILE:-}" ] && [ -r /etc/ssl/certs/ca-certificates.crt ]; then echo "NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" >> "$GITHUB_ENV" fi # Fail here rather than 8 minutes into a build if a restored store is # unusable. - name: Check nix works run: nix --extra-experimental-features "nix-command flakes" --version # The installer does not enable flakes, so `nix build .#…` needs the # features passed explicitly — a local machine that has them in # nix.conf will build fine while CI fails without this. # # This is the step that guards pnpmDeps.hash, and it is cheap, so it runs # on every event. - name: build SPA package run: | nix --extra-experimental-features "nix-command flakes" build \ .#webFrontend \ --no-link # The release binaries are a full LTO compile of the workspace. The rust # job already builds, lints and tests the same sources, so the only extra # signal here is that the *packaging* works — worth a few minutes on main, # not on every PR push. Trade-off: a break that is specific to the Nix # packaging surfaces on main instead of on the PR. - name: build release packages if: github.event_name == 'push' run: | nix --extra-experimental-features "nix-command flakes" build \ .#maestro \ --no-link # Saved only from a push, so a PR that built webFrontend alone can never # write a store missing the Rust closure and have a later run restore it # through the prefix key. - name: Save nix store if: github.event_name == 'push' && steps.nix-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: /nix key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'web/pnpm-lock.yaml', '**/*.nix') }}