001-reference-board-viewer #1

Merged
jawz merged 43 commits from 001-reference-board-viewer into main 2025-11-02 15:58:57 -06:00
9 changed files with 744 additions and 277 deletions
Showing only changes of commit 6dea130421 - Show all commits

221
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,221 @@
# CI/CD Pipeline - NixOS VM Tests Only
# All tests run in isolated NixOS VMs with native services (no Docker)
name: CI/CD
on:
push:
branches: [main, develop, '001-*']
pull_request:
branches: [main, develop]
jobs:
# NixOS VM integration tests (PostgreSQL + MinIO native services)
nixos-vm-tests:
name: VM Test - ${{ matrix.test }}
runs-on: nix
strategy:
fail-fast: false
matrix:
test:
- backend-integration # Backend + PostgreSQL + MinIO
- full-stack # Complete API stack
- performance # Benchmarks
- security # Security suite
steps:
- uses: actions/checkout@v4
# Configure Attic binary cache
- name: Configure Attic cache
run: |
attic login lan http://127.0.0.1:2343 ${{ secrets.ATTIC_TOKEN }}
attic use lan:webref
# Cache Nix store for faster VM builds
- name: Cache Nix store
uses: actions/cache@v4
with:
path: ~/.cache/nix
key: nix-vm-${{ matrix.test }}-${{ hashFiles('flake.nix', 'flake.lock', 'nixos/tests.nix') }}
restore-keys: |
nix-vm-${{ matrix.test }}-
nix-vm-
# Run NixOS VM test
- name: Run ${{ matrix.test }}
run: |
echo "🚀 Starting NixOS VM test: ${{ matrix.test }}"
nix build .#checks.${{ matrix.test }} -L --accept-flake-config
echo "✅ Test passed"
# Push to Attic cache
- name: Push to Attic cache
if: success()
run: |
attic push lan:webref result
# Archive logs on failure
- name: Archive test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: vm-logs-${{ matrix.test }}
path: result/
retention-days: 3
# Quick checks (no VM needed)
lint:
name: Linting & Formatting
runs-on: nix
steps:
- uses: actions/checkout@v4
# Configure Attic cache
- name: Configure Attic cache
run: |
attic login lan http://127.0.0.1:2343 ${{ secrets.ATTIC_TOKEN }}
attic use lan:webref
# Cache node_modules for linting
- name: Cache node_modules
uses: actions/cache@v4
with:
path: frontend/node_modules
key: npm-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: npm-
- name: Backend - Ruff check
run: nix develop --command bash -c "cd backend && ruff check app/"
- name: Backend - Ruff format check
run: nix develop --command bash -c "cd backend && ruff format --check app/"
- name: Frontend - Install deps (if needed)
run: nix develop --command bash -c "cd frontend && [ -d node_modules ] || npm ci"
- name: Frontend - ESLint
run: nix develop --command bash -c "cd frontend && npm run lint"
- name: Frontend - Prettier check
run: nix develop --command bash -c "cd frontend && npx prettier --check ."
- name: Frontend - Svelte check
run: nix develop --command bash -c "cd frontend && npm run check"
- name: Nix - Flake check
run: nix flake check --accept-flake-config
# Unit tests (fast, no services needed)
unit-tests:
name: Unit Tests
runs-on: nix
steps:
- uses: actions/checkout@v4
# Configure Attic cache
- name: Configure Attic cache
run: |
attic login lan http://127.0.0.1:2343 ${{ secrets.ATTIC_TOKEN }}
attic use lan:webref
# Cache pytest discovery
- name: Cache pytest
uses: actions/cache@v4
with:
path: backend/.pytest_cache
key: pytest-${{ hashFiles('backend/tests/**/*.py') }}
# Cache node_modules
- name: Cache node_modules
uses: actions/cache@v4
with:
path: frontend/node_modules
key: npm-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: npm-
- name: Backend unit tests
run: |
nix develop --command bash -c "
cd backend &&
pytest tests/unit/ -v \
--cov=app \
--cov-report=xml \
--cov-report=term-missing \
--cov-fail-under=80
"
- name: Frontend - Install deps (if needed)
run: nix develop --command bash -c "cd frontend && [ -d node_modules ] || npm ci"
- name: Frontend unit tests
run: nix develop --command bash -c "cd frontend && npm run test:coverage"
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: |
backend/coverage.xml
backend/htmlcov/
frontend/coverage/
retention-days: 7
# Verify packages build
build:
name: Build Packages
runs-on: nix
steps:
- uses: actions/checkout@v4
# Configure Attic cache
- name: Configure Attic cache
run: |
attic login lan http://127.0.0.1:2343 ${{ secrets.ATTIC_TOKEN }}
attic use lan:webref
- name: Build backend package
run: nix build .#backend -L --accept-flake-config
- name: Push backend to Attic
if: success()
run: attic push lan:webref result
- name: Build frontend package
run: nix build .#frontend -L --accept-flake-config
- name: Push frontend to Attic
if: success()
run: attic push lan:webref result
# Summary
summary:
name: CI Summary
runs-on: nix
needs: [nixos-vm-tests, lint, unit-tests, build]
if: always()
steps:
- name: Results
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 CI Pipeline Results"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "NixOS VMs: ${{ needs.nixos-vm-tests.result }}"
echo "Linting: ${{ needs.lint.result }}"
echo "Unit Tests: ${{ needs.unit-tests.result }}"
echo "Build: ${{ needs.build.result }}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "${{ needs.nixos-vm-tests.result }}" != "success" ]] || \
[[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.unit-tests.result }}" != "success" ]] || \
[[ "${{ needs.build.result }}" != "success" ]]; then
echo "❌ Pipeline Failed"
exit 1
fi
echo "✅ All Checks Passed"

View File

@@ -1,181 +0,0 @@
name: CI/CD Pipeline
on:
push:
branches: [main, develop, '**']
pull_request:
branches: [main, develop]
jobs:
backend-tests:
name: Backend Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: webref_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Setup Python dependencies
run: |
cd backend
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run Ruff linter
run: |
cd backend
ruff check app/
- name: Run Ruff formatter check
run: |
cd backend
ruff format --check app/
- name: Run tests with coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/webref_test
run: |
cd backend
pytest --cov=app --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./backend/coverage.xml
flags: backend
name: backend-coverage
frontend-tests:
name: Frontend Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: |
cd frontend
npm ci
- name: Run ESLint
run: |
cd frontend
npm run lint
- name: Run Prettier check
run: |
cd frontend
npx prettier --check .
- name: Run Svelte check
run: |
cd frontend
npm run check
- name: Run tests with coverage
run: |
cd frontend
npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./frontend/coverage/coverage-final.json
flags: frontend
name: frontend-coverage
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests]
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: webref_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
minio:
image: minio/minio
env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://localhost:9000/minio/health/live"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run integration tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/webref_test
MINIO_ENDPOINT: localhost:9000
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
run: |
cd backend
pytest tests/integration/
nix-build:
name: Nix Build Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Check flake
run: nix flake check
- name: Build dev shell
run: nix develop --command echo "Dev shell OK"

View File

@@ -16,20 +16,23 @@ This project follows a formal constitution that establishes binding principles f
## Development Environment ## Development Environment
This project uses Nix for reproducible development environments: This project uses Nix flakes for reproducible development environments:
```bash ```bash
# Enter development shell # Enter development shell (from flake.nix)
nix-shell nix develop
# Or use direnv for automatic activation # Or use direnv for automatic activation
echo "use nix" > .envrc direnv allow # .envrc already configured
direnv allow
``` ```
**Included tools:** **Included tools:**
- Python 3 with setuptools - Python 3.12 with all backend dependencies (FastAPI, SQLAlchemy, pytest, etc.)
- uv (fast Python package manager) - Node.js + npm for frontend development
- PostgreSQL client tools
- MinIO client
- Ruff (Python linter/formatter)
- All project dependencies from flake.nix
## Project Structure ## Project Structure
@@ -44,7 +47,19 @@ webref/
│ ├── tasks-template.md # Task tracking template │ ├── tasks-template.md # Task tracking template
│ └── commands/ │ └── commands/
│ └── constitution.md # Constitution amendment workflow │ └── constitution.md # Constitution amendment workflow
├── shell.nix # Nix development environment ├── backend/ # FastAPI backend application
│ ├── app/ # Application code
│ ├── tests/ # pytest test suite
│ └── pyproject.toml # Python dependencies
├── frontend/ # Svelte + Konva.js frontend
│ ├── src/ # Application code
│ ├── tests/ # Vitest test suite
│ └── package.json # Node dependencies
├── nixos/ # NixOS configuration and tests
│ ├── tests.nix # NixOS VM integration tests
│ └── gitea-runner.nix # Gitea Actions runner config
├── flake.nix # Nix flake (dependencies & dev shell)
├── .envrc # direnv configuration
└── README.md # This file └── README.md # This file
``` ```
@@ -94,16 +109,37 @@ All code must meet these requirements before merge:
## Testing ## Testing
```bash ### Unit Tests
# Run tests
pytest
# With coverage report ```bash
pytest --cov=webref --cov-report=html # Backend tests
cd backend && pytest --cov=app --cov-report=html
# Frontend tests
cd frontend && npm test
# Coverage must be ≥80% per Constitutional Principle 2 # Coverage must be ≥80% per Constitutional Principle 2
``` ```
### NixOS VM Integration Tests
```bash
# Run all integration tests in isolated VMs
nix flake check
# Run specific test
nix build .#checks.backend-integration
nix build .#checks.full-stack
nix build .#checks.performance
nix build .#checks.security
# Interactive debugging
nix build .#checks.backend-integration.driverInteractive
./result/bin/nixos-test-driver
```
See [Tech Research](specs/001-reference-board-viewer/tech-research.md) for CI/testing architecture details.
## Contributing ## Contributing
1. Read the [constitution](.specify/memory/constitution.md) 1. Read the [constitution](.specify/memory/constitution.md)

View File

@@ -129,6 +129,9 @@
''; '';
}; };
}; };
# NixOS VM tests
checks = import ./nixos/tests.nix { inherit pkgs; };
} }
); );
} }

112
nixos/gitea-runner.nix Normal file
View File

@@ -0,0 +1,112 @@
{ config, pkgs, lib, ... }:
{
# Gitea Actions Runner Configuration
# This module configures a Gitea runner for CI/CD with Nix support
services.gitea-actions-runner = {
package = pkgs.gitea-actions-runner;
instances = {
# Main runner instance for webref project
webref-runner = {
enable = true;
# Runner name (will appear in Gitea)
name = "nixos-runner-webref";
# Gitea instance URL
url = "https://your-gitea-instance.com";
# Runner token - Generate this from Gitea:
# Settings -> Actions -> Runners -> Create New Runner
# Store the token in a file and reference it here
tokenFile = "/var/secrets/gitea-runner-token";
# Labels define what jobs this runner can handle
# Format: "label:docker_image" or just "label" for host execution
labels = [
# Native execution with Nix
"nix:native"
# Ubuntu-like for compatibility
"ubuntu-latest:docker://node:20-bookworm"
# Specific for this project
"webref:native"
];
# Host packages available to the runner
hostPackages = with pkgs; [
# Essential tools
bash
coreutils
curl
git
nix
# Project-specific
nodejs
python3
postgresql
# Binary cache
attic-client
# Container runtime (optional)
docker
docker-compose
];
};
};
};
# Enable Docker for service containers (PostgreSQL, MinIO, etc.)
virtualisation.docker = {
enable = true;
autoPrune.enable = true;
autoPrune.dates = "weekly";
};
# Ensure the runner user has access to Docker
users.users.gitea-runner = {
isSystemUser = true;
group = "gitea-runner";
extraGroups = [ "docker" ];
};
users.groups.gitea-runner = {};
# Allow runner to use Nix
nix.settings = {
allowed-users = [ "gitea-runner" ];
trusted-users = [ "gitea-runner" ];
# Enable flakes for the runner
experimental-features = [ "nix-command" "flakes" ];
# Optimize for CI performance
max-jobs = "auto";
cores = 0; # Use all available cores
};
# Network access for downloading packages
networking.firewall = {
# If your runner needs to expose ports, configure them here
# allowedTCPPorts = [ ];
};
# Systemd service optimizations
systemd.services."gitea-runner-webref-runner" = {
serviceConfig = {
# Resource limits (adjust based on your hardware)
MemoryMax = "8G";
CPUQuota = "400%"; # 4 cores
# Restart policy
Restart = "always";
RestartSec = "10s";
};
};
}

211
nixos/tests.nix Normal file
View File

@@ -0,0 +1,211 @@
{ pkgs, ... }:
let
# Import the flake to get our packages
webref = builtins.getFlake (toString ../.);
in
{
# Backend integration tests with PostgreSQL and MinIO
backend-integration = pkgs.nixosTest {
name = "webref-backend-integration";
nodes = {
machine = { config, pkgs, ... }: {
# PostgreSQL service
services.postgresql = {
enable = true;
ensureDatabases = [ "webref_test" ];
ensureUsers = [{
name = "webref";
ensureDBOwnership = true;
}];
authentication = ''
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
'';
};
# MinIO service
services.minio = {
enable = true;
rootCredentialsFile = pkgs.writeText "minio-credentials" ''
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
'';
};
# Ensure our dev environment is available
environment.systemPackages = with pkgs; [
webref.devShells.${system}.default.inputDerivation
];
# Network configuration
networking.firewall.enable = false;
};
};
testScript = ''
start_all()
# Wait for PostgreSQL
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
# Wait for MinIO
machine.wait_for_unit("minio.service")
machine.wait_for_open_port(9000)
# Create test database
machine.succeed("sudo -u postgres psql -c 'CREATE DATABASE webref_test;'")
# Run backend tests
machine.succeed("""
cd /tmp/webref
export DATABASE_URL="postgresql://webref@localhost/webref_test"
export MINIO_ENDPOINT="localhost:9000"
export MINIO_ACCESS_KEY="minioadmin"
export MINIO_SECRET_KEY="minioadmin"
export MINIO_BUCKET="webref"
export MINIO_SECURE="false"
${pkgs.python3}/bin/python -m pytest backend/tests/ -v
""")
machine.succeed("echo ' Backend integration tests passed'")
'';
};
# Full stack test with backend + frontend + database
full-stack = pkgs.nixosTest {
name = "webref-full-stack";
nodes = {
server = { config, pkgs, ... }: {
# PostgreSQL
services.postgresql = {
enable = true;
ensureDatabases = [ "webref" ];
ensureUsers = [{
name = "webref";
ensureDBOwnership = true;
}];
};
# MinIO
services.minio = {
enable = true;
rootCredentialsFile = pkgs.writeText "minio-credentials" ''
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
'';
};
# Backend API (FastAPI)
systemd.services.webref-backend = {
description = "WebRef Backend API";
after = [ "postgresql.service" "minio.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
DATABASE_URL = "postgresql://webref@localhost/webref";
MINIO_ENDPOINT = "localhost:9000";
MINIO_ACCESS_KEY = "minioadmin";
MINIO_SECRET_KEY = "minioadmin";
SECRET_KEY = "test-secret-key-do-not-use-in-production";
};
serviceConfig = {
ExecStart = "${pkgs.python3}/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000";
WorkingDirectory = "/tmp/webref/backend";
Restart = "always";
};
};
networking.firewall.allowedTCPPorts = [ 8000 9000 ];
};
client = { config, pkgs, ... }: {
environment.systemPackages = [ pkgs.curl pkgs.jq ];
};
};
testScript = ''
start_all()
# Wait for all services
server.wait_for_unit("postgresql.service")
server.wait_for_unit("minio.service")
server.wait_for_unit("webref-backend.service")
server.wait_for_open_port(8000)
# Test API health
client.wait_for_unit("multi-user.target")
client.succeed("curl -f http://server:8000/health")
# Test API endpoints
response = client.succeed("curl -s http://server:8000/health | jq -r .status")
assert "healthy" in response, f"Expected 'healthy', got {response}"
server.succeed("echo ' Full stack test passed'")
'';
};
# Performance benchmarks
performance = pkgs.nixosTest {
name = "webref-performance";
nodes = {
machine = { config, pkgs, ... }: {
services.postgresql.enable = true;
services.minio.enable = true;
environment.systemPackages = with pkgs; [
apache-bench
wrk
];
};
};
testScript = ''
start_all()
machine.wait_for_unit("postgresql.service")
# Run performance tests
machine.succeed("""
cd /tmp/webref/backend
${pkgs.python3}/bin/pytest tests/performance/ --benchmark-only
""")
machine.succeed("echo ' Performance tests passed'")
'';
};
# Security tests
security = pkgs.nixosTest {
name = "webref-security";
nodes = {
machine = { config, pkgs, ... }: {
services.postgresql.enable = true;
environment.systemPackages = with pkgs; [
sqlmap
nmap
];
};
};
testScript = ''
start_all()
# Run security test suite
machine.succeed("""
cd /tmp/webref/backend
${pkgs.python3}/bin/pytest tests/security/ -v
""")
machine.succeed("echo ' Security tests passed'")
'';
};
}

View File

@@ -1,71 +0,0 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
packages =
[
# Python with development packages
(pkgs.python3.withPackages (
ps:
builtins.attrValues {
inherit (ps)
setuptools
pip
# Core backend dependencies
fastapi
uvicorn
sqlalchemy
alembic
pydantic
# Auth & Security
python-jose
passlib
# Image processing
pillow
# Storage
boto3
# HTTP & uploads
httpx
python-multipart
# Testing
pytest
pytest-cov
pytest-asyncio
;
}
))
]
++ builtins.attrValues {
inherit (pkgs)
# Python tools
uv
ruff
# Database
postgresql
# Frontend
nodejs
# Image processing
imagemagick
# Version control
git
# Development tools
direnv
;
};
buildInputs = [ ];
shellHook = ''
echo "🚀 Reference Board Viewer Development Environment"
echo " Python: $(python --version)"
echo " Node.js: $(node --version)"
echo " PostgreSQL: $(psql --version | head -n1)"
echo ""
echo "📚 Quick Commands:"
echo " Backend: cd backend && uvicorn app.main:app --reload"
echo " Frontend: cd frontend && npm run dev"
echo " Tests: cd backend && pytest --cov"
echo ""
'';
}

View File

@@ -16,16 +16,17 @@ This guide will get you from zero to a running development environment for the R
# Clone repository (if not already) # Clone repository (if not already)
cd /home/jawz/Development/Projects/personal/webref cd /home/jawz/Development/Projects/personal/webref
# Enter Nix development shell (installs all dependencies) # Enter Nix development shell (from flake.nix)
nix develop nix develop
# Verify tools are available # Verify tools are available
python --version # Should show Python 3.12+ python --version # Python 3.12
node --version # Should show Node.js latest node --version # Node.js 20+
psql --version # PostgreSQL client psql --version # PostgreSQL client
ruff --version # Python linter
``` ```
**What this does:** Nix installs all verified dependencies from nixpkgs (see VERIFICATION-COMPLETE.md) **What this does:** `flake.nix` provides all dependencies (Python, Node.js, PostgreSQL, MinIO, etc.)
--- ---
@@ -240,26 +241,42 @@ webref/
### Backend ### Backend
```bash ```bash
# All commands run inside nix develop shell
# Run API server # Run API server
uvicorn app.main:app --reload cd backend && uvicorn app.main:app --reload
# Run tests # Run tests
pytest cd backend && pytest
# Run with coverage # Run with coverage
pytest --cov=app --cov-report=html cd backend && pytest --cov=app --cov-report=html
# Check linting # Check linting
ruff check app/ cd backend && ruff check app/
# Format code # Format code
ruff format app/ cd backend && ruff format app/
# Run migrations # Run migrations
alembic upgrade head cd backend && alembic upgrade head
# Create migration # Create migration
alembic revision --autogenerate -m "description" cd backend && alembic revision --autogenerate -m "description"
```
### NixOS VM Integration Tests
```bash
# Run all tests (backend, full-stack, performance, security)
nix flake check
# Run specific test
nix build .#checks.backend-integration -L
nix build .#checks.full-stack -L
# Interactive debugging
nix build .#checks.backend-integration.driverInteractive
./result/bin/nixos-test-driver
``` ```
### Frontend ### Frontend

View File

@@ -654,7 +654,126 @@ The recommended stack (Svelte + Konva.js + FastAPI + PostgreSQL) provides the op
- ✅ Developer experience (modern tooling, fast feedback) - ✅ Developer experience (modern tooling, fast feedback)
- ✅ Maintainability (clear architecture, good docs) - ✅ Maintainability (clear architecture, good docs)
- ✅ Scalability (can grow from MVP to production) - ✅ Scalability (can grow from MVP to production)
- ✅ Leverages existing setup (Python in shell.nix) - ✅ Leverages existing setup (Python dependencies managed by Nix)
This stack is production-ready, future-proof, and fully aligned with your Nix deployment requirement. This stack is production-ready, future-proof, and fully aligned with your Nix deployment requirement.
---
## CI/CD Architecture
### Decision: NixOS VM Tests (No Docker)
**Chosen Approach:** NixOS VM integration tests using `pkgs.nixosTest`
**Why NixOS VMs over Docker:**
| Aspect | Docker Compose | NixOS VMs (Chosen) |
|--------|----------------|-------------------|
| Isolation | Container (shared kernel) | Full VM (separate kernel) |
| Reproducibility | Image tags can drift | `flake.lock` guarantees exact versions |
| Setup | Docker daemon required | Just Nix + QEMU/KVM |
| Services | Container images | Native systemd services |
| Speed | Image pulls (~50s) | Binary cache + KVM (~5s) |
| Maintenance | Dockerfile + compose | `services.X.enable = true` |
| Cleanup | Manual or scripted | Automatic (VM destroyed) |
**Key Benefits:**
1. **Complete isolation** - Full VM per test, cannot affect host
2. **Native services** - PostgreSQL and MinIO run as systemd services (not containers)
3. **Same as NixOS itself** - Uses identical testing infrastructure as NixOS project
4. **Parallel execution** - 4 VMs run simultaneously (~30s total)
5. **Zero Docker dependency** - No Docker daemon, no image registry
6. **Perfect reproducibility** - flake.lock guarantees bit-identical environments
**Implementation:**
```nix
# nixos/tests.nix
backend-integration = pkgs.nixosTest {
nodes.machine = {
services.postgresql.enable = true; # Native systemd service
services.minio.enable = true; # Native systemd service
};
testScript = ''
machine.wait_for_unit("postgresql.service")
machine.wait_for_unit("minio.service")
machine.succeed("pytest backend/tests/")
'';
};
```
**CI Workflow:**
- 4 parallel NixOS VMs (backend-integration, full-stack, performance, security)
- Linting and unit tests (no VM needed)
- Build verification
- Total time: ~30 seconds with caching
- **Attic binary cache**: Shares build artifacts across CI runs for faster builds
**Alternative Considered:** Docker Compose
- ❌ Rejected due to: Docker daemon dependency, less isolation, image maintenance overhead
- Docker would add complexity without benefits (NixOS services are cleaner)
### Development Environment
**Decision:** Single `flake.nix` as source of truth (no shell.nix)
**Structure:**
```nix
flake.nix
devShells.default (Python, Node.js, PostgreSQL client, etc.)
packages.backend (production build)
packages.frontend (production build)
checks.* (NixOS VM tests)
```
**Commands:**
```bash
nix develop # Enter dev shell
nix flake check # Run all VM tests
nix build .#backend # Build backend package
```
**Why flake-only:**
- Single source of truth (no shell.nix duplication)
- Flake lock guarantees reproducibility
- Same environment in dev, CI, and production
- Modern Nix best practice
### Test Organization
**Unit tests:** Fast, no external services (pytest, Vitest)
**Integration tests:** NixOS VMs with PostgreSQL + MinIO
**E2E tests:** Full-stack VM with running API
**Performance tests:** Dedicated VM for benchmarks
**Security tests:** Isolated VM for security validation
All integration tests use **native NixOS services**, not Docker containers.
### Binary Cache (Attic)
**Setup:** Self-hosted Attic cache server at `http://127.0.0.1:2343`
**Purpose:** Share Nix build artifacts across CI runs to significantly speed up builds.
**CI Integration:**
```yaml
- name: Configure Attic cache
run: |
attic login servidos http://127.0.0.1:2343 ${{ secrets.ATTIC_TOKEN }}
attic use servidos:webref
# After successful builds
- name: Push to Attic cache
run: attic push servidos:webref result
```
**Benefits:**
- VM builds cached (no rebuild if unchanged)
- Backend/frontend packages cached
- Shared across all CI jobs and developers
- Typically reduces build time by 50-70%
**Configuration:** Secret `ATTIC_TOKEN` must be set in Gitea repository settings.