Ana içeriğe geç

CI/CD Pipeline

CI Workflow (ci.yml)

Her push ve PR'da otomatik çalışır:

1. backend-lint

  • ruff check app/ — Linting
  • ruff format --check app/ — Format kontrolü
  • mypy app/ --ignore-missing-imports — Type check

2. backend-test

  • PostgreSQL 15 (TimescaleDB) + Redis 7 servisler başlatılır
  • alembic upgrade head — Migration'lar uygulanır (+ reversibility testi: downgrade -1upgrade head)
  • pytest tests/ -v --cov=app -m "not integration" — Unit + REST testleri
  • Blocking integration adımları (gerçek DB)@pytest.mark.integration testleri "not integration" filtresinde atlandığı için her biri ayrı blocking adım olarak koşar (Issue #615 sahte-yeşil önleme). PR-7 ile eklenen:
    • tests/integration/test_invitations_db.py — kullanıcı davet yaşam döngüsünün gerçek-DB kanıtları: resend sonrası eski token geçersiz/yeni token geçerli, aynı token ikinci accept 400 (atomik tek-kullanım), pending-davet partial unique → 409, role_ids JSONB round-trip, cross-tenant revoke 404 (hiç-yazmadan), accept-409 rollback'inin token'ı yakmaması. Yeşil olmadan merge edilemez.

Migration-runtime fail-closed guard (issue #970)

Migration-runtime testleri (tests/fixtures/migration_runtime.py kullanan 12 dosya) DATABASE_URL yoksa _REQUIRES_DB skipif marker'ı ile collection anında atlanır — adım rc=0 ile yeşil görünür ama tek bir gerçek-DB assertion'ı koşmaz. Bu, Issue #615 sahte-yeşil sınıfıdır.

Ortak modül bunu import anında engeller:

  • Ne zaman ateşlenir: CI (veya GITHUB_ACTIONS) truthy ve DATABASE_URL postgresql değil/boş → RuntimeError, adım kırmızı.
  • Lokalde: CI sinyali yoksa guard hiç çalışmaz; mevcut pytest.skip davranışı korunur.
  • Bu mesajı görürsen: ilgili adımın env: bloğuna postgres servisine işaret eden DATABASE_URL'i ekle. Postgres servisi olmayan bir job'a (örn. migration-sanity, sqlite kullanır) bu dosyaları collect eden bir pytest adımı eklenmemeli.
  • Yeni migration-runtime testi eklerken: dosyaya ayrı bir blocking adım + env: bloğunda DATABASE_URL şart.
  • Guard'ın kurulu olduğu ve tetikleyicisinin CI'da mevcut olduğu tests/unit/test_migration_runtime_fixture.py ile her koşumda doğrulanır.

3. firmware-test

  • pio test -e native — PlatformIO unit testleri

4. frontend-lint

  • npm run lint — ESLint
  • npx tsc --noEmit — TypeScript kontrolü

5. frontend-test (PR #276 sonrası eklendi — BLOCKING)

  • npm test — Vitest, 43 test
  • Kapsam (WS refactor release gate):
    • frontend/src/config/site.test.tsgetWsUrl() SSR/localhost/prod/env override
    • frontend/src/lib/websocket/client.test.ts — mixed-content guard + ctor exception
    • frontend/src/lib/websocket/context.test.tsx — Provider defansif init + cleanup
    • frontend/src/lib/websocket/WebSocketErrorBoundary.test.tsx — boundary fallback
  • Yeşil olmazsa merge engellenir.

6. frontend-build

  • npm run build — Production build
  • Önemli: NEXT_PUBLIC_WS_URL artık build-arg olarak verilmiyor (PR #276). Detay: Frontend Kritik Notlar.

7. docker-build (sadece PR)

  • Docker Buildx ile tek-platform build (runner native amd64, push yok)

E2E Workflow (e2e.yml)

PR-7 (tenant onboarding) ile yeniden yapılandırıldı: e2e artık gerçek backend stack üzerinde koşar (önceden: mock API URL ile yalnız render smoke + @ci tag'li test yokken "no tests found → exit 0" vacuous-pass).

Tetikleyiciler

  • PR: frontend/**, e2e/**, backend/app/features/{signup,users,tenants}/**, backend/app/core/security/**, backend/scripts/seed_e2e_superadmin.py, workflow dosyası
  • Nightly: 02:00 UTC
  • Manuel: workflow_dispatch (base_url verilirse ayrı prod-smoke job'ı — read-only production smoke)

@ci onboarding journeys (chromium) job akışı

  1. Servisler: timescale/timescaledb:latest-pg15 (host 5433) + redis:7-alpine (host 6380). Redis şart: signup OTP durumu Redis'te yaşar.
  2. Backend: Python 3.11 + pip install -r requirements.txtalembic upgrade head (BACKFILL_* placeholder env, ci.yml emsali) → python scripts/seed_e2e_superadmin.py (idempotent superadmin seed; production'da fail-closed) → uvicorn 127.0.0.1:8000 + /healthz readiness.
  3. Frontend: npm cinpm run build (BACKEND_URL=http://127.0.0.1:8000 build-time bake — next.config.js rewrites /api/*'ı uvicorn'a proxy'ler) → next start -p 3000.
  4. Playwright: e2e/ self-contained (e2e/package.json + e2e/playwright.config.ts) → npx playwright test --grep @ci --project=chromium.

Backend e2e ortam env'leri

EnvDeğerNot
ENVIRONMENTtestSeed + echo guard'ları production'da fail-closed
TENANT_SIGNUP_ENABLEDtrueFail-closed master flag — yalnız bu CI ortamında açılır
SIGNUP_TEST_OTP_ECHOtrueOTP/e-posta/davet token'ları debug_* alanlarında echo edilir. Yalnız CI/e2e — backend çifte korumalı (production'da flag yok sayılır) ve deploy.yml bu flag'i bilerek wire etmez
CELERY_ALWAYS_EAGERtrueTask'lar inline; dispatch'ler try/except sarılı — SMTP'siz ortamda istek 500'e dönmez
MQTT_ENABLEDfalseBroker gerekmez
E2E_SUPERADMIN_EMAIL/PASSWORDsabit CI değeriSeed script ve spec'ler aynı değeri kullanır (e2e/fixtures/onboarding.ts sözleşmesi)

Vacuous-pass kaldırıldı

--grep @ci koşumu artık "no tests found" durumunda fail eder (Playwright varsayılanı, --pass-with-no-tests verilmez). Yeşil rozet = en az 1 gerçek journey koştu. Kapsanan journey'ler: e2e/specs/signup-approval.spec.ts (signup → OTP → e-posta doğrulama → superadmin onayı → yeni tenant admin login) ve e2e/specs/invitation-accept.spec.ts (davet oluştur → kabul → yeni kullanıcı login).

Port / concurrency kararı

Job, self-hosted X64 host'unda sabit portlar bağlar (5433/6380/8000/3000) → job-level concurrency lock e2e-stack-ports-runner (cancel-in-progress: false, ci.yml backend-test-port-5432-runner emsal deseni) ile aynı anda tek e2e stack'i koşar. ci.yml'nin 5432 lock grubuna katılmadı: Backend Tests artık ubuntu-latest'te (izole VM) — ortak host yok, katılmak e2e'yi alakasız job'ların arkasına kuyruklardı. Postgres host portu 5433 seçildi: 5432 hem ci.yml emsalinin tarihsel çakışma portu hem de runner host'unda yerel Postgres'in en olası portu.

Required check değil

@ci onboarding journeys (chromium) job'ı main ruleset'inde required status check değildir (mevcut required listesi: Backend Lint/Tests, Frontend Lint/Build, Firmware, Alembic single-head, Nginx guard, Docker Build). Required'a çevirmek bilinçli kullanıcı kararıdır.


Deploy Workflow (deploy.yml)

CI başarılı olunca otomatik tetiklenir:

  1. SSH setup — webfactory/ssh-agent
  2. rsync — Dosya senkronizasyonu (exclude: .git, node_modules, .env, certs)
  3. .env oluştur — GitHub Secrets'tan
  4. TLS kontrol — SSL sertifika varlığı
  5. Infrastructure start — postgres, redis, emqx, minio
  6. Migrationalembic upgrade ${MIGRATION_TARGET:-head} (PR #277 sonrası varsayılan head)
  7. Image pull — GHCR'dan amd64 image'lar çekilir (build CI runner'da yapılır; bkz. "Build CI Runner Migration")
  8. Container startdocker compose up -d
  9. Health check — Backend, frontend, nginx HTTPS (post-check 18×5s polling)
  10. Rollback guard — post-check fail ederse :previous tag'e otomatik geri dönüş
  11. Monitoring cron — health-check, container-monitor kurulumu

Soft-env writer'lar (opsiyonel secret → .env)

.env ana bloğundan sonra bir dizi soft-env adımı koşar: GitHub Secret set edilmişse ilgili satır .env'e eklenir, set edilmemişse hiçbir şey yazılmaz ve docker-compose.yml default'ları geçerli kalır (deploy kırılmaz — PR #449 dersi: zorunlu olmayan env hard-gate'e eklenmez). Mevcut gruplar: MQTT, SMTP, Driver Self-Service canary, OSOS KEK ve PR-7 ile eklenen tenant onboarding flag'leri:

SecretCompose defaultDavranış
TENANT_SIGNUP_ENABLEDfalseSet edilmedikçe public /signup yüzeyi kapalı kalır (fail-closed master flag)
PLATFORM_ADMIN_EMAILSboşYeni başvuru bildirimi e-posta hedefleri (virgülle ayrılmış)
PLATFORM_ADMIN_PHONESboşYeni başvuru bildirimi WhatsApp hedefleri

SIGNUP_TEST_OTP_ECHO bilerek wire edilmez: OTP/token echo flag'i yalnız CI/e2e içindir; production .env'sine taşıyan bir yol deploy.yml'de açılmaz (secret tanımlansa dahi yazılmaz).

Release Workflow (release.yml)

Git tag push (v*) ile tetiklenir:

  • GitHub release + changelog oluşturma
  • ghcr.io'ya Docker image push (tag + latest)

amd64 Build

Prod sunucusu (ZeusTR, x86_64, TR/İstanbul) amd64. CI/CD pipeline 2026-06-09 itibarıyla yalnız linux/amd64 build üretir; tüm job'lar [self-hosted, linux, X64] runner'da koşar. ARM build tamamen kaldırıldı (geri dönüş planlanmıyor).

Tarihçe: 2026-05-19..2026-06-09 arası prod ARM64 Hetzner CAX üzerindeydi ve pipeline yalnız linux/arm64 üretiyordu. Başkent EDAŞ MDM API'sinin yurtdışı/datacenter IP'lere uyguladığı port-443 coğrafi/ASN bloğu nedeniyle Zeus, x86_64 TR sunucuya (ZeusTR) taşındı; build mimarisi de amd64'e döndü. QEMU adımı kaldırıldı (native build'de no-op idi).

CI Konfigürasyonu

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max

Build Timeout

Multi-arch döneminde (2026-05 öncesi) QEMU emülasyonu frontend build'i 25-30 dakikaya çıkarıyordu. Native-only (tek mimari) geçişle native hız geri kazanıldı; timeout değerleri üst sınır güvenliği olarak korunuyor (cache cold start veya runner state pollution için marj):

JobTimeout (üst sınır)Tipik süre (warm cache)
backend build60 dk< 5 dk
frontend build60 dk< 8 dk
modbus-poller build45 dk< 3 dk
whatsapp-bridge build45 dk< 2 dk

CAX (ARM64) — Hetzner Geçişi (tarihsel, 2026-05)

Bu bölüm tarihseldir. 2026-06-09'da Zeus, ZeusTR (x86_64, TR) sunucusuna taşındı; aşağıdaki CAX/ARM64 karşılaştırması artık aktif mimariyi tanımlamaz, geçmiş kararın kaydıdır.

KriterCPX (x86)CAX (ARM64)
Fiyat (eşdeğer kapasite)Yüksek~%30 daha düşük
RAM (eşdeğer fiyat)4GB8GB
TimescaleDB performansStabilStabil (testler geçti)
Build platformlinux/amd64,linux/arm64linux/arm64 (multi-arch kaldırıldı 2026-05-19)

CAX'e geçiş özellikle 4GB → 8GB RAM upgrade'i için tercih edilmişti (TimescaleDB OOM-kill çözümü).


Build CI Runner Migration

Daha önce frontend/backend image build'leri deploy SSH session içinde yapılıyordu. Bu yaklaşımın sorunları:

  • SSH connection timeout → silent build failure
  • Sunucuda CPU/RAM kullanımı build sırasında piklenir, prod servisler etkilenir
  • Build cache sunucu disk'inde tutulur → disk şişmesi
  • Hata yakalama zayıf (output buffer kesilebilir)

Yeni Akış

  1. CI runner'da image build → ghcr.io'ya push
  2. Deploy adımı sadece docker pull + docker compose up -d çalıştırır
  3. Build hataları CI'da yakalanır, deploy hiç başlamaz
- name: Build & push backend
uses: docker/build-push-action@v6
with:
context: ./backend
platforms: linux/amd64
push: true
tags: |
ghcr.io/gucluceyhan/zeus-backend:latest
ghcr.io/gucluceyhan/zeus-backend:${{ github.sha }}

Tag Rotation + Rollback

Her başarılı build üç tag ile push'lanır:

TagAnlam
:latestEn son başarılı build
:previousBir önceki başarılı build (rollback hedefi)
:<sha>Commit-spesifik tag (immutable referans)

Rollback Akışı

Production'da bir sorun yakalandığında:

# Hızlı rollback (önceki sürüme dön)
docker pull ghcr.io/gucluceyhan/zeus-backend:previous
docker tag ghcr.io/gucluceyhan/zeus-backend:previous \
ghcr.io/gucluceyhan/zeus-backend:latest
docker compose up -d backend

:previous tag'i her başarılı deploy'da otomatik güncellenir (bir önceki :latest rotate edilir). Manuel rollback için :<sha> tag'i de kullanılabilir:

docker pull ghcr.io/gucluceyhan/zeus-backend:abc1234

Post-deploy Health Check

Deploy sonrası 90 saniye boyunca her 5 saniyede bir container sağlığı kontrol edilir. Bu süre içinde container healthy olmazsa otomatik rollback tetiklenir.

TIMEOUT=90
INTERVAL=5
ELAPSED=0

while [ $ELAPSED -lt $TIMEOUT ]; do
STATUS=$(docker inspect --format='{{.State.Status}}' backend)
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' backend)

if [ "$STATUS" = "running" ] && [ "$HEALTH" = "healthy" ]; then
echo "Deploy başarılı (${ELAPSED}s)"
exit 0
fi

sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

echo "Deploy başarısız — rollback başlatılıyor"
docker tag ghcr.io/gucluceyhan/zeus-backend:previous \
ghcr.io/gucluceyhan/zeus-backend:latest
docker compose up -d backend
exit 1

Health check Dockerfile'da tanımlanır (her servis için ayrı): HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD curl -f http://localhost:8000/health || exit 1


MIGRATION_TARGET — Politika (PR #277 ile güncellendi)

Yeni varsayılan: head

PR #277 ile deploy.yml:244 satırındaki MIGRATION_TARGET varsayılanı merge_sofar_mqtt'ten head'e taşındı. Önceden 0037 ve 0038 gibi yeni revisionlar otomatik deploy'a giremiyordu — workflow_dispatch override veya manuel SSH apply gerektiriyordu. Bu durum production drift riski (kod yeni şemayı bekler, DB eski şemada kalır) yaratıyordu.

Mevcut akış

# deploy.sh içinde — CI workflow tarafından sunucuya yazılıyor
MIGRATION_TARGET="${MIGRATION_TARGET:-head}"
$DOCKER_COMPOSE run --rm backend alembic upgrade "${MIGRATION_TARGET}"

head güvenle kullanılabilir çünkü single-head invariant CI guard hâlâ aktif (migration-sanity job, aşağıda). Multiple heads durumunda deploy başlamadan PR/merge engellenir.

Override politikası — ne zaman kullanılır

MIGRATION_TARGET'ı spesifik revision'a pin'lemek için iki yöntem:

  1. workflow_dispatch input — manuel deploy tetiklerken MIGRATION_TARGET=0037_add_ocpp_tables ver.
  2. Repo env değişkenivars.MIGRATION_TARGET set edilirse ${MIGRATION_TARGET:-head} fallback'i devreden çıkar.

Hangi durumda override:

SenaryoHedefGerekçe
Hot-fix, yeni migration'ı staging'de test etmek istiyorsunBir önceki revision (örn. 0037_add_ocpp_tables)Yeni migration'ı sadece prod'a değil staging'de önce uygula
Rollback sonrası alembic_version stuckSpesifik revision idManuel kontrolü garanti et
Production'da rutin deployhead (varsayılan)Otomatik akış — override yapma
Production'da gereksiz override yapma

Production'da MIGRATION_TARGET override etmek migration drift'in başlangıç noktasıdır. head varsayılanı ve migration-sanity guard'ı ile birlikte güvenli. Ancak yeni bir migration emergency rollback gerektiriyorsa kısa süreliğine spesifik revision'a pin'leyip ardından kaldırın.


migration-sanity Job (single-head invariant)

CI pipeline'ında pre-deploy doğrulama olarak çalışır. Multiple heads varsa deploy adımı tetiklenmez.

migration-sanity:
runs-on: [self-hosted, linux, X64]
steps:
- run: |
HEADS=$(alembic heads | wc -l)
if [ "$HEADS" -ne 1 ]; then
echo "ERROR: Multiple alembic heads — merge migration gerekli"
alembic heads
exit 1
fi

Bu guard tek satır head garantisi sağlar. Detay: Migration Rehberi — Multiple Heads.