Skip to main content

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:

  1. Previous Docker image tags are restored
  2. Services restart with the previous version
  3. API health is re-verified
  4. 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

WorkflowTriggerPurpose
qa-tests.ymlPR (web/api changes)Playwright QA with 4-shard parallelism
lighthouse-ci.ymlPR (web changes)Desktop + mobile Lighthouse scores
performance-pr-gate.ymlPR (api changes)k6 load test, P95 < 500ms gate
staging-spike-test.ymlManual5× spike test with FR-041 compliance
security-dependency-scan.ymlWeekly + PRnpm audit + Trivy + SBOM generation
security-secret-scan.ymlPRGitLeaks + CodeQL scanning
security-zap-baseline.ymlManualOWASP 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