Skip to content

feat(specialist): display jobs prominently on dashboard and auto-appr… #111

feat(specialist): display jobs prominently on dashboard and auto-appr…

feat(specialist): display jobs prominently on dashboard and auto-appr… #111

Workflow file for this run

name: Backend CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
concurrency:
group: backend-ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
# Monorepo (npm workspaces): install va build ILDIZDAN. Dummy DB URL'lar —
# schema `directUrl` env talab qiladi; `prisma validate/generate` hech narsaga
# ULANMAYDI. JWT/TOTP — Bosqich 23-audit tuzatishi: `generate:contracts`
# (`emit-openapi.ts`) `NestFactory.create(AppModule)` chaqiradi, bu esa
# `ConfigModule.forRoot()`ning Zod env validatsiyasini ishga tushiradi
# (`app.init()` EMAS — DB/Redis ULANMAYDI, faqat MAJBURIY maydonlar
# formati tekshiriladi). Bu uchtasi bo'lmasa "quality" job HAR DOIM
# yiqilardi — aynan shu sabab bilan (haqiqiy CI loglarida tasdiqlangan,
# `STAFF_TOTP_ENCRYPTION_KEY: Required`). Qiymatlar sir EMAS — faqat CI
# formatini qondirish uchun, hech qanday real muhitda ishlatilmaydi.
env:
DATABASE_URL: postgresql://bobododa_app:app@localhost:5432/bobododa?schema=public
DATABASE_MIGRATION_URL: postgresql://bobododa_migrator:migrator@localhost:5432/bobododa?schema=public
REDIS_URL: redis://localhost:6379
JWT_ACCESS_SECRET: ci-quality-job-dummy-access-secret-32
JWT_STAFF_ACCESS_SECRET: ci-quality-job-dummy-staff-secret-32
STAFF_TOTP_ENCRYPTION_KEY: '2222222222222222222222222222222222222222222222222222222222222222'
jobs:
quality:
name: Lint · Typecheck · Unit · Contracts
runs-on: ubuntu-latest
outputs:
run_backend: ${{ steps.scope.outputs.run }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
# release-engineer dizayni (PR #20 deadlock tuzatishi) — bu job
# ENDI HAR DOIM ishga tushadi (path filter olib tashlandi, pastga
# qarang), lekin faqat backend'ga DAXLDOR o'zgarish bo'lsa qimmat
# qadamlarni bajaradi. MUHIM: filtr JOB darajasida `if:` orqali EMAS
# — GitHub SKIPPED job'ni "muvaffaqiyatli" deb hisoblaydi va bu
# aynan taqiqlangan "vakuum yashil" bo'lardi. Shuning uchun bu job
# HAR DOIM tugallanadi (haqiqiy SUCCESS/FAILURE bilan), faqat ICHKI
# QADAMLAR shartli o'tkazib yuboriladi.
- name: O'zgarish ko'lami — backend'ga daxldormi?
id: scope
run: |
set -euo pipefail
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "non-PR event (${{ github.event_name }}) -> run=true (to'liq)" >&2
echo "run=true" >> "$GITHUB_OUTPUT"
echo "### Backend CI — to'liq ishga tushdi (${{ github.event_name }})" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git rev-parse --verify -q HEAD^2 >/dev/null
files=$(git diff --no-renames --name-only HEAD^1 HEAD)
result=$(printf '%s\n' "$files" | bash scripts/ci-changed-scope.sh backend | tee /dev/stderr | tail -1)
case "$result" in
run=true|run=false) ;;
*) echo "::error::ci-changed-scope.sh kutilmagan chiqish berdi: $result" && exit 1 ;;
esac
echo "$result" >> "$GITHUB_OUTPUT"
{
echo "### Backend CI — o'zgarish ko'lami"
echo "Base: \`$(git rev-parse HEAD^1)\` · Head (PR): \`$(git rev-parse HEAD^2)\` · Merge: \`$(git rev-parse HEAD)\`"
echo "\`\`\`"
printf '%s\n' "$files"
echo "\`\`\`"
echo "**$result**"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/setup-node@v4
if: steps.scope.outputs.run != 'false'
with:
node-version: 22
cache: npm
- name: Install (workspaces, root)
if: steps.scope.outputs.run != 'false'
run: npm ci
- name: Prisma validate + generate
if: steps.scope.outputs.run != 'false'
run: |
npm run prisma:generate --workspace backend
npm exec --workspace backend -- prisma validate
- name: Lint (backend)
if: steps.scope.outputs.run != 'false'
run: npm run lint --workspace backend
- name: Typecheck (backend + contracts)
if: steps.scope.outputs.run != 'false'
run: npm run typecheck --workspace backend && npm run typecheck --workspace @bobododa/contracts
- name: Unit tests (backend)
if: steps.scope.outputs.run != 'false'
run: npm run test:cov --workspace backend
- name: Build (backend)
if: steps.scope.outputs.run != 'false'
run: npm run build --workspace backend
- name: generate:contracts (OpenAPI → TS types + enums)
if: steps.scope.outputs.run != 'false'
run: |
npm run generate:contracts
test -s packages/contracts/openapi.json
test -s packages/contracts/src/enums.ts
- name: Contracts drift — generated artifact’lar commit qilingan holat bilan bir xil bo‘lishi shart (bo‘lim 74)
if: steps.scope.outputs.run != 'false'
run: |
if ! git diff --quiet -- packages/contracts; then
echo "::error::packages/contracts eskirgan — 'npm run generate:contracts' natijasi commit qilingan holatdan farq qiladi. Lokal'da ishga tushirib, natijani commit qiling."
git diff --stat -- packages/contracts
exit 1
fi
# Bo'lim 25 cross-review topilmasi (qa-engineer) — yuqoridagi drift
# gate FAQAT "emitter o'zi bilan committed holatni" solishtiradi:
# `emit-openapi.ts`dan `setGlobalPrefix` chaqiruvi yana olib
# tashlansa-yu committed fayl "generate:contracts" bilan qayta
# yozilsa, ikkalasi ham prefikssiz bo'lib, yuqoridagi gate BUZILGAN
# holatni "toza" deb hisoblardi. Bu qadam kontraktning O'ZINI,
# emitter'dan mustaqil, tekshiradi.
- name: OpenAPI prefix invariant — barcha yo'l /api/v1 bilan boshlanishi shart (health'dan tashqari)
if: steps.scope.outputs.run != 'false'
run: npm run check:openapi-prefix
- name: Skipped by scope — dalil
if: steps.scope.outputs.run == 'false'
run: |
echo "::notice title=Backend CI skipped by scope::O'zgargan fayllar backend'ga daxldor emas — to'liq tekshiruv o'tkazib yuborildi (tafsilot yuqoridagi summary'da)."
integration:
name: Integration (real Postgres 16 + Redis — service containers)
runs-on: ubuntu-latest
needs: quality
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
TZ: UTC
PGTZ: UTC
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 20
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s --health-timeout 5s --health-retries 20
env:
# e2e testlar shu xizmatlarga ulanadi (Testcontainers EMAS — u ba'zi
# runner'larda Redis'ni "Connection is closed" bilan yiqitardi).
E2E_SUPERUSER_URL: postgresql://postgres:postgres@localhost:5432/postgres
E2E_REDIS_URL: redis://localhost:6379
# F2 — infra kutilgan edi lekin topilmadi (masalan env nomi xato
# yozilsa yoki service konteyner ko'tarilmasa) JIMGINA skip bo'lib,
# CI yashil qolib ketmasin: shu flag yoqilganda testlar SKIP o'rniga
# YIQILADI ("infra topilmadi"). Lokal ishda flag yo'q — qulay skip.
CI_REQUIRE_E2E: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
if: needs.quality.outputs.run_backend != 'false'
with:
node-version: 22
cache: npm
- name: Install (workspaces, root)
if: needs.quality.outputs.run_backend != 'false'
run: npm ci
- name: Prisma generate
if: needs.quality.outputs.run_backend != 'false'
run: npm run prisma:generate --workspace backend
- name: Migration test — rollarni bootstrap qilib, fresh DB'ga migratsiya (bo'lim 73)
if: needs.quality.outputs.run_backend != 'false'
working-directory: backend
env:
PGPASSWORD: postgres
run: |
psql "postgresql://postgres:postgres@localhost:5432/postgres" -c "CREATE DATABASE ci_migration_check;"
# DIQQAT — parollar ATAYLAB 'app'/'migrator': rollar
# (bobododa_app/bobododa_migrator) Postgres'da KLASTER-GLOBAL, shu
# SHARED service konteynerni keyinroq shu jobdagi "e2e" qadami ham
# ishlatadi (har bir spec'ning beforeAll'i `provisionDb()` orqali).
# `provisionDb()` (test/support/e2e-infra.ts) shu rollarni
# `IF NOT EXISTS` bilan yaratadi va parolni QATTIQ 'app'/'migrator'
# deb oladi — agar shu yerda BOSHQA parol ishlatilsa, bu qadam
# rolni O'SHA parol bilan BIRINCHI bo'lib yaratib qo'yadi va
# `provisionDb()`dagi CREATE `IF NOT EXISTS` tufayli o'tkazib
# yuboriladi — keyin "e2e" qadami P1000 (Authentication failed)
# bilan yiqiladi (real CI run'da topilgan va tasdiqlangan xato).
psql "postgresql://postgres:postgres@localhost:5432/ci_migration_check" \
-v app_pw=app \
-v migrator_pw=migrator \
-v db_name=ci_migration_check \
-f prisma/sql/roles.sql
export DATABASE_URL="postgresql://bobododa_app:app@localhost:5432/ci_migration_check?schema=public"
export DATABASE_MIGRATION_URL="postgresql://bobododa_migrator:migrator@localhost:5432/ci_migration_check?schema=public"
npx prisma migrate deploy
STATUS=$(npx prisma migrate status)
echo "$STATUS"
echo "$STATUS" | grep -q "Database schema is up to date" || (echo "::error::Migratsiyalar joriy emas" && exit 1)
- name: Production-config test — fail-closed qoidalar HAQIQIY boot bilan tasdiqlanadi (bo'lim 73)
if: needs.quality.outputs.run_backend != 'false'
working-directory: backend
env:
DATABASE_URL: postgresql://bobododa_app:app@localhost:5432/ci_migration_check?schema=public
DATABASE_MIGRATION_URL: postgresql://bobododa_migrator:migrator@localhost:5432/ci_migration_check?schema=public
REDIS_URL: redis://localhost:6379
JWT_ACCESS_SECRET: ci-production-config-test-access-secret-32
JWT_STAFF_ACCESS_SECRET: ci-production-config-test-staff-secret-32
STAFF_TOTP_ENCRYPTION_KEY: '1111111111111111111111111111111111111111111111111111111111111111'
NODE_ENV: production
# Bo'lim 3 (admin.bobododa.uz) — STAFF_CORS_ORIGINS ham production
# superRefine'da tekshiriladi (https, CORS_ORIGINS bilan ustma-ust
# tushmasin). Bu qator qo'shilmasa "to'liq to'g'ri production
# config" bilan boot HAR DOIM muvaffaqiyatsiz bo'ladi — ikkalasi
# ham sukutda bir xil (http://localhost:3000) qoladi.
CORS_ORIGINS: https://app.bobododa.uz
STAFF_CORS_ORIGINS: https://admin.bobododa.uz
SWAGGER_ENABLED: 'false'
run: |
BOOT_CMD="node --require ts-node/register/transpile-only --require tsconfig-paths/register scripts/boot-check.ts"
if PAYMENT_PROVIDER=TEST $BOOT_CMD; then
echo "::error::production'da PAYMENT_PROVIDER=TEST bilan boot MUVAFFAQIYATLI bo'ldi — fail-closed himoya buzilgan"
exit 1
fi
echo "OK: PAYMENT_PROVIDER=TEST production'da to'g'ri rad etildi"
# Bo'lim 87/§88 — Payme to'g'ri, lekin SMS_PROVIDER hali CONSOLE
# (sukut) bo'lsa production HAMON ko'tarilmasligi kerak.
if PAYMENT_PROVIDER=PAYME PAYME_MERCHANT_ID=ci-merchant PAYME_LOGIN=Paycom PAYME_KEY=ci-key PAYME_CHECKOUT_URL=https://test.paycom.uz $BOOT_CMD; then
echo "::error::production'da SMS_PROVIDER=CONSOLE bilan boot MUVAFFAQIYATLI bo'ldi — fail-closed himoya buzilgan"
exit 1
fi
echo "OK: to'liq Payme config bilan HAM SMS_PROVIDER=CONSOLE production'da to'g'ri rad etildi"
# Bosqich 13 — TO'LIQ to'g'ri config (Payme + PlayMobile + payouts
# xavfsiz o'chirilgan) bilan production ENDI MUVAFFAQIYATLI
# ko'tarilishi SHART (fresh-DB dry-run bilan qo'lda tasdiqlangan —
# RUNBOOK §13). Bu — "fail-closed FAQAT emas, balki to'g'ri config
# bilan HAQIQATAN ishga tushadi" musbat tomonni tekshiradi.
if ! PAYMENT_PROVIDER=PAYME PAYME_MERCHANT_ID=ci-merchant PAYME_LOGIN=Paycom PAYME_KEY=ci-key PAYME_CHECKOUT_URL=https://test.paycom.uz \
SMS_PROVIDER=PLAYMOBILE PLAYMOBILE_API_URL=https://send.example.uz/broker-api PLAYMOBILE_LOGIN=ci-login PLAYMOBILE_PASSWORD=ci-password PLAYMOBILE_SENDER=BoboDoda \
PAYOUTS_ENABLED=false \
$BOOT_CMD; then
echo "::error::to'liq to'g'ri production config bilan boot MUVAFFAQIYATSIZ bo'ldi"
exit 1
fi
echo "OK: to'liq to'g'ri production config (Payme + PlayMobile + payouts o'chirilgan) bilan boot MUVAFFAQIYATLI"
# Bosqich 23 — real Payme credential hali yo'q bo'lgan production
# launch uchun: PAYMENTS_ENABLED=false (PAYMENT_PROVIDER=TEST
# sukutda qolsa ham) production'da MUVAFFAQIYATLI ko'tarilishi
# SHART — "soxta TEST fallback" emas, ATAYLAB xavfsiz o'chirilgan
# holat (PAYOUTS_ENABLED bilan bir xil falsafa).
if ! PAYMENTS_ENABLED=false \
SMS_PROVIDER=PLAYMOBILE PLAYMOBILE_API_URL=https://send.example.uz/broker-api PLAYMOBILE_LOGIN=ci-login PLAYMOBILE_PASSWORD=ci-password PLAYMOBILE_SENDER=BoboDoda \
PAYOUTS_ENABLED=false \
$BOOT_CMD; then
echo "::error::PAYMENTS_ENABLED=false bilan boot MUVAFFAQIYATSIZ bo'ldi — xavfsiz to'lovsiz launch buzilgan"
exit 1
fi
echo "OK: PAYMENTS_ENABLED=false (Payme credentialsiz) bilan boot MUVAFFAQIYATLI — xavfsiz to'lovsiz launch"
# Bo'lim 25 — DEV_EXPOSE_OTP hech qachon Zod darajasida rad
# etilmagan edi (faqat runtime `shouldExposeDevOtp` uni inert
# qilardi). To'liq to'g'ri (PLAYMOBILE, PAYOUTS xavfsiz o'chirilgan)
# production config bilan HAM DEV_EXPOSE_OTP=true boot'ni
# yiqitishi SHART — real boot bilan tasdiqlangan, faqat unit test
# emas.
#
# security-engineer cross-review topilmasi — `PAYOUTS_ENABLED=false`
# ATAYLAB qo'shildi va natija matni `DEV_EXPOSE_OTP` so'zi bo'yicha
# tekshiriladi: buni qo'shmasdan qolgan bo'lsak, boot PAYOUT_PROVIDER=
# TEST (sukut) production'da fail-closed bo'lgani uchun HAR HOLDA
# yiqilar edi — DEV_EXPOSE_OTP=true ta'sirini umuman TEKSHIRMAGAN
# holda ham bu qadam "to'g'ri" ko'rinardi (vakuum tekshiruv).
BOOT_OUTPUT=$(PAYMENT_PROVIDER=PAYME PAYME_MERCHANT_ID=ci-merchant PAYME_LOGIN=Paycom PAYME_KEY=ci-key PAYME_CHECKOUT_URL=https://test.paycom.uz \
SMS_PROVIDER=PLAYMOBILE PLAYMOBILE_API_URL=https://send.example.uz/broker-api PLAYMOBILE_LOGIN=ci-login PLAYMOBILE_PASSWORD=ci-password PLAYMOBILE_SENDER=BoboDoda \
PAYOUTS_ENABLED=false \
DEV_EXPOSE_OTP=true \
$BOOT_CMD 2>&1) && BOOT_EXIT=0 || BOOT_EXIT=1
echo "$BOOT_OUTPUT"
if [ "$BOOT_EXIT" -eq 0 ]; then
echo "::error::production'da DEV_EXPOSE_OTP=true bilan boot MUVAFFAQIYATLI bo'ldi — fail-closed himoya buzilgan"
exit 1
fi
if ! echo "$BOOT_OUTPUT" | grep -q "DEV_EXPOSE_OTP"; then
echo "::error::boot DEV_EXPOSE_OTP TUFAYLI EMAS, boshqa sabab bilan yiqildi — bu tekshiruv o'z maqsadini isbotlamayapti"
exit 1
fi
echo "OK: production'da DEV_EXPOSE_OTP=true (to'liq to'g'ri qolgan config bilan ham) aynan DEV_EXPOSE_OTP sababli to'g'ri rad etildi"
- name: e2e — health + DB-role append-only (A4)
if: needs.quality.outputs.run_backend != 'false'
run: npm run test:e2e --workspace backend
# Bo'lim 73 — 10x chinakam parallel HTTP burst standart GitHub
# Actions 2 vCPU runner'ida transport darajasida (ECONNRESET) beqaror
# bo'lib chiqdi (uch mustaqil maqsadli tuzatish kamaytirmadi — RUNBOOK
# §24), majburiy gate'ni takroran to'sib qo'yardi — holbuki yuqoridagi
# majburiy suite'dagi "Concurrent double-submit" AYNAN SHU CAS
# invariant'ni (unique constraint — bitta qator) 2x parallel bilan
# ISHONCHLI tekshiradi. Shu 10x versiya signal sifatida SAQLANADI
# (`continue-on-error: true` — muvaffaqiyatsizlik job'ni QIZARTIRMAYDI,
# lekin GitHub UI'da alohida ko'rinadi), majburiy gate'dan chiqarildi.
- name: e2e-stress — 10x parallel seller burst (informational, non-blocking)
if: needs.quality.outputs.run_backend != 'false'
continue-on-error: true
run: npm run test:e2e:stress --workspace backend
- name: Skipped by scope — dalil
if: needs.quality.outputs.run_backend == 'false'
run: |
echo "::notice title=Backend integration skipped by scope::'quality' job'i o'zgarishlarni backend'ga daxldor emas deb aniqladi — integratsiya testlari o'tkazib yuborildi."