CI/CD Pipeline
The project uses GitHub Actions with a self-hosted runner on Hetzner (8 vCPU, 16GB RAM). The CI server is shared across multiple projects.
CI Pipeline (Pull Requests)
Workflow: .github/workflows/ci.yml
Triggers: Pull requests on main and develop
detect-changes (2 min)
│
├──► quality-gates (20 min)
│ ├── pnpm install + prisma generate + build:packages
│ ├── typecheck + lint + format (parallel)
│ ├── unit tests (~14s, 4500+ tests)
│ └── scheduling engine tests
│
├──► db-tests (25 min) — skipped for dependency-only PRs
│ ├── start ephemeral postgres (Docker)
│ ├── setup RLS roles + extensions
│ ├── run migrations
│ ├── integration tests
│ ├── E2E tests
│ └── cleanup container
│
└──► build verification (25 min) — skipped for dependency-only PRs
├── build API
├── build Web
└── build Worker
Dependency-Only PR Optimization
PRs that only change pnpm-lock.yaml, package.json, or .github/dependabot.yml skip database tests and build verification. Quality gates (lint, typecheck, unit tests) always run.
Parallel Steps
Typecheck, lint, and format check run concurrently within a single step using background processes. This saves ~1-2 minutes per run while staying on a single runner.
Dependabot Handling
Dependabot PRs share a single concurrency slot and queue without cancellation, ensuring each PR gets validated.
Deploy Pipeline (Production)
Workflow: .github/workflows/deploy-production.yml
Triggers: Push to main, manual workflow_dispatch
test (CI server) deploy (prod server)
├── lint + typecheck (parallel) ├── version tag (YYYY.MM.DD.PATCH)
├── unit tests ├── stop services
├── scheduling tests ├── build Docker images
├── integration tests* ├── database backup + migrate
├── E2E tests* ├── restart services
└── cleanup ├── health check (API + web)
├── smoke test (Playwright)
* skipped with skip_tests flag └── rollback on failure
skip_tests Flag
Use skip_tests: true via workflow_dispatch to skip integration/E2E tests for hotfixes. Lint, typecheck, and unit tests always run. Default is false.
Versioning
Date-based semver: v2026.03.22.1 (year.month.day.patch). Tags are created and pushed automatically.
Rollback
If the post-deploy smoke test fails:
- Previous Docker image tags are restored
- Services restart with the previous version
- API health is re-verified
- Teams notification sent
Docker Build
Multi-stage Dockerfile (Dockerfile.prod) with shared dependency layer:
deps (install + prisma + build packages)
├── api-builder → api (port 3001)
├── web-builder → web (port 3000)
└── worker
BuildKit cache mount speeds up pnpm installs across builds.
Other Workflows
| Workflow | Trigger | Purpose |
|---|---|---|
qa-tests.yml | PR (web/api changes) | Playwright QA with 4-shard parallelism |
lighthouse-ci.yml | PR (web changes) | Desktop + mobile Lighthouse scores |
performance-pr-gate.yml | PR (api changes) | k6 load test, P95 < 500ms gate |
staging-spike-test.yml | Manual | 5× spike test with FR-041 compliance |
security-dependency-scan.yml | Weekly + PR | npm audit + Trivy + SBOM generation |
security-secret-scan.yml | PR | GitLeaks + CodeQL scanning |
security-zap-baseline.yml | Manual | OWASP ZAP baseline scan |
Self-Hosted Runner
- Location: Hetzner, 8 vCPU / 16GB RAM
- Runner path:
/opt/actions-runner/ - Prod repo:
/opt/booking/ - Node: Managed via fnm
- Labels:
self-hosted(CI),[self-hosted, booking-prod](deploy)
Disk Cleanup
CI jobs automatically prune Docker images older than 7 days when disk usage exceeds 80%.
Local CI
Run the full CI pipeline locally:
pnpm ci:local # Full CI
pnpm ci:local:quick # Quick mode (skip slow tests)
Or individual steps:
pnpm typecheck
pnpm lint
pnpm test:api:unit
pnpm test:api:int # Requires test DB on port 5433
pnpm test:api:e2e # Requires test DB on port 5433