feat(specialist): display jobs prominently on dashboard and auto-appr… #111
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |