Mert Özen Her Satırda Daha İleri
DevOps GitHub Actions — #06

GitHub Actions ile Angular CI: setup-node, npm ci, Lint, Vitest, Build Budget'ları ve Playwright

Mert Özen 8 Kas 2026 12 dk 4 görüntülenme
GitHub Actions ile Angular CI: setup-node, npm ci, Lint, Vitest, Build Budget'ları ve Playwright

Angular 22 CI'ında Node sürümünü setup-node@v7 ile package.json'daki devEngines'ten oku, npm ci ile lock dosyasına bağlı kur, ng lint, Vitest ve budget'lı ng build çalıştır, Playwright e2e'yi API ve PostgreSQL service container'ıyla koştur. Monorepo'da setup-node'un otomatik npm cache'inin neden açılmadığını, lock drift hatasını, Node sürüm uyumsuzluğunu ve tarayıcı cache'inin etkisini act çıktılarıyla gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler act 0.2.97, actionlint 1.7.12, Docker 29.2, Node 24 (yerelde 24.22.2), npm 11.19, Angular 22.2, angular-eslint 22.6, Vitest 5.1, Playwright 1.65, .NET 10 ve PostgreSQL 17 ile denenmiştir. Workflow'lar act ile yerelde koşuldu; GitHub'da koşulmadı.

Önce Tahmin Et

Ada, Bookstore'un Angular uygulamasında tarih biçimlendirmek için bir paket istiyor. package.json'ı editörde açıp dependencies altına "date-fns": "^4.1.0" satırını ekliyor, kendi makinesinde npm install çalıştırıyor, uygulama açılıyor ve değişikliği commit'liyor. Yalnız package-lock.json commit'e girmiyor. CI'da (Continuous Integration) npm ci çalıştığında ne olur? Paketi kurup geçer mi, uyarı verip geçer mi, yoksa durur mu?

$ npm ci
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Missing: date-fns@4.4.0 from lock file

Durur. npm ci, npm install'ın CI için tasarlanmış kardeşi: node_modules'u silip lock dosyasındaki ağacı birebir kurar ve package.json ile lock dosyası uyuşmuyorsa kurmayı reddeder. Bu, önceki yazıdaki NuGet --locked-mode'un npm karşılığı. Bu yazıda bookstore-web için web-ci.yml'ı kuruyoruz: Node sürümü, npm ci ve cache, ESLint, Vitest, build budget'ları ve API'yi (Application Programming Interface) PostgreSQL ile birlikte ayağa kaldıran Playwright e2e testleri.

Node Sürümü: setup-node@v7, node-version-file ve devEngines

Angular 22'nin desteklediği Node sürümleri @angular/cli'ın engines alanında yazıyor: ^22.22.3 || ^24.15.0 || >=26.0.0. Uymayan bir Node ile CLI (Command Line Interface) hiç başlamıyor. Bu makinede eski bir Node 22 kurulumuyla denediğimizde:

$ node node_modules/@angular/cli/bin/ng.js version
Node.js version v22.2.0 detected.
The Angular CLI requires a minimum Node.js version of v22.22.3 or v24.15.0 or v26.0.0.

Please update your Node.js version or visit https://nodejs.org/ for additional instructions.

Node sürümünü workflow'a node-version: 24 diye yazmak yerine projenin kendisine koyuyoruz. web/package.json'a bir devEngines bölümü ekledik:

  "packageManager": "npm@11.19.1",
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "^24.15.0"
    }
  }

actions/setup-node@v7'in dokümantasyonuna göre node-version-file'a bir package.json verdiğinde sırayla volta.node, devEngines.runtime (adı node olan girdi) ve engines.node alanlarına bakıyor. Böylece geliştiricinin editörü, npm ve CI aynı yerden okuyor. .nvmrc ya da .node-version dosyası da aynı işi görür; önemli olan sürümün tek bir dosyada olması.

  lint-test-build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    defaults:
      run:
        working-directory: web
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version-file: web/package.json
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --watch=false
      - run: npm run build
      - uses: actions/upload-artifact@v7
        with:
          name: web-dist
          path: web/dist/bookstore-web/browser/
          retention-days: 7

defaults.run.working-directory: web bütün run: step'lerini web/ klasöründe çalıştırıyor; ama bu ayar uses: step'lerine uygulanmaz. Bu yüzden node-version-file ve artifact path'i repository köküne göre yazıldı.

npm ci ve npm Cache: Monorepo'da Otomatik Cache Neden Açılmadı?

Workflow'un ilk hâlinde cache için hiçbir şey yazmadık. setup-node'un README'si v6'dan beri package.json'daki packageManager ya da devEngines.packageManager alanı npm'i gösteriyorsa npm cache'inin otomatik açıldığını söylüyor ve Angular CLI yeni projelere "packageManager": "npm@11.19.1" satırını zaten yazıyor. Ama act log'unda cache'e dair tek bir satır yoktu:

[web-ci/lint-test-build] ⭐ Run Main actions/setup-node@v7
[web-ci/lint-test-build]   | Resolved web/package.json as ^24.15.0
[web-ci/lint-test-build]   | Found in cache @ /opt/hostedtoolcache/node/24.22.2/arm64
[web-ci/lint-test-build]   | node: v24.22.2
[web-ci/lint-test-build]   | npm: 11.19.1
[web-ci/lint-test-build]   ✅  Success - Main actions/setup-node@v7 [1.059691708s]
[web-ci/lint-test-build] ⭐ Run Main npm ci
[web-ci/lint-test-build]   | added 422 packages, and audited 423 packages in 28s

Node sürümü web/package.json'dan doğru okundu (^24.15.0 → 24.22.2), ama npm cache'i devreye girmedi. Sebebi setup-node v7.1.0'ın kaynak kodunda: packageManager alanını yalnız $GITHUB_WORKSPACE/package.json'da, yani repository kökünde arıyor. Bizim package.json'ımız web/ altında; kökte hiç yok. Monorepo'da cache'i açıkça istemek gerekiyor:

      - uses: actions/setup-node@v7
        with:
          node-version-file: web/package.json
          cache: npm
          cache-dependency-path: web/package-lock.json
[web-ci/lint-test-build]   | npm cache is not found
[web-ci/lint-test-build]   ⚙  ::set-output:: cache-hit=false
[web-ci/lint-test-build]   ✅  Success - Main npm ci [37.979782917s]
...
[web-ci/lint-test-build]   | Cache Size: ~81 MB (85241513 B)
[web-ci/lint-test-build]   | Cache saved with the key: node-cache-Linux-arm64-npm-ae784366ea1f37547f023535ee13d80784f121cd646ec659bcc63f8118c01ad2
...
[web-ci/e2e            ]   | Cache restored from key: node-cache-linux-arm64-npm-ae784366ea1f37547f023535ee13d80784f121cd646ec659bcc63f8118c01ad2
[web-ci/e2e            ]   ⚙  ::set-output:: cache-hit=true
[web-ci/e2e            ]   ✅  Success - Main npm ci [7.835933041s]

İlk job'da cache yoktu ve npm ci 38 saniye sürdü; job sonunda 81 MB'lık cache kaydedildi. Aynı run'daki e2e job'ı onu geri yükledi ve npm ci 7,8 saniyeye indi.

Cache'lenen şey node_modules değil, npm'in global cache klasörü; npm ci her seferinde node_modules'u yeniden kurar ama paketleri internetten değil cache'ten alır. Key, lock dosyasının hash'inden üretilir. README'nin bir uyarısı var: yüksek yetkili ya da hassas bilgiye erişen workflow'larda, cache gerçekten gerekmiyorsa otomatik cache'i package-manager-cache: false ile kapatmanı öneriyor. Cache poisoning konusunu güvenlik yazısında açacağız; bizim CI workflow'umuz contents: read ile çalıştığı için burada açık bıraktık.

Lint: ng lint ve angular-eslint

Angular CLI yeni projelere linter kurmuyor; ng add angular-eslint ile ekledik. Komut workspace'in Angular 22 olduğunu görüp angular-eslint 22'yi kurdu, bir eslint.config.js oluşturdu ve angular.json'a lint target'ını ekledi. Kuralı ihlal eden bir değişiklik, örneğin bir service'te constructor injection kullanmak:

$ npx ng lint
…/src/app/book.service.ts
  15:15  error  Prefer using the inject() function over constructor parameter injection. Use Angular's migration schematic to automatically refactor: ng generate @angular/core:inject  @angular-eslint/prefer-inject

✖ 1 problem (1 error, 0 warnings)

Lint errors found in the listed files.

Lint'i test'ten önce koşturuyoruz, çünkü saniyeler sürüyor ve çoğu basit hatayı test ortamını hazırlamadan yakalıyor. Kural setini ekip olarak sen seçersin; önemli olan CI'ın yerelde çalışan komutun aynısını (npm run lint) çalıştırması.

Unit Test: ng test, Vitest ve Watch Modu

Angular 21'den beri yeni projelerde ng test'in varsayılan runner'ı Vitest; Karma ile tarayıcı açmak yerine testler jsdom içinde koşuyor. Bizim projede de angular.json'daki test target'ı @angular/build:unit-test builder'ını kullanıyor ve runner varsayılan olarak vitest. CI açısından önemli soru watch modu. Builder'ın şemasında watch için açıklama şu: TTY (TeleTYpewriter, etkileşimli terminal) ortamlarında varsayılan true, diğerlerinde false. GitHub runner'ında terminal etkileşimli değil, yani varsayılan olarak tek seferlik koşar; biz yine de --watch=false'u açıkça yazdık, çünkü TTY ayıran bir ortamda (bazı self-hosted runner kurulumları, docker run -t) aynı komut asla bitmeyen bir job'a dönüşür.

[web-ci/lint-test-build] ⭐ Run Main npm test -- --watch=false
[web-ci/lint-test-build]   | > ng test --watch=false
[web-ci/lint-test-build]   |  RUN  v5.1.0 …/web
[web-ci/lint-test-build]   |  ✓  bookstore-web  src/app/app.spec.ts (2 tests) 33ms
[web-ci/lint-test-build]   |  Test Files  1 passed (1)
[web-ci/lint-test-build]   |       Tests  2 passed (2)
[web-ci/lint-test-build]   ✅  Success - Main npm test -- --watch=false [1.822684792s]
[web-ci/lint-test-build]   ⚙  Summary - ## Vitest Test Report

### Summary

- **Test Files**: ✅ **1 pass** · 1 total
- **Test Results**: ✅ **2 passes** · 2 total

Son satırlar ilginç: bu job summary'yi biz yazmadık. Vitest, GitHub Actions ortamında olduğunu anlayınca (GITHUB_ACTIONS değişkeni; act de bunu true yapıyor) GITHUB_STEP_SUMMARY'ye kendi raporunu ekledi. Üçüncü yazıda .NET testleri için elle kurduğumuz mekanizmanın aynısı.

Unit testler HttpTestingController ile API çağrısını sahte bir cevapla karşılıyor; gerçek API'ye ihtiyaç duymuyor. Gerçek API ile konuşan test e2e katmanında.

Build ve Budget'lar

ng build production configuration'ında angular.json'daki budget'ları kontrol ediyor: initial bundle için 500 kB'ta warning, 1 MB'ta error. Bu sınırların neden var olduğunu Angular serisinin "Angular Build ve Deploy" yazısında anlatmıştık. CI açısından önemi şu: budget aşımı bir build hatası, yani pull request'i kırmızıya çevirir. Etkisini görmek için error sınırını geçici olarak 120 kB'a indirdik:

$ npx ng build
Application bundle generation failed. [1.349 seconds] - 2026-11-08T04:10:42.877Z

▲ [WARNING] bundle initial exceeded maximum budget. Budget 100.00 kB was not met by 43.94 kB with a total of 143.94 kB.


✘ [ERROR] bundle initial exceeded maximum budget. Budget 120.00 kB was not met by 23.94 kB with a total of 143.94 kB.

Budget'ı CI'da bir kapı olarak kullanmanın asıl değeri, büyümenin fark edilmeden birikmesini engellemek. Bir pull request initial bundle'ı 40 kB büyütüyorsa bunu review'da konuşmak, üç ay sonra "uygulama neden yavaş açılıyor" diye geriye dönük aramaktan ucuzdur. Build çıktısı web-dist adıyla artifact olarak yükleniyor; bir sonraki yazıda bu artifact'ı API'nin wwwroot'una taşıyacağız.

Playwright ile e2e: API ve PostgreSQL ile Birlikte

Unit testler bileşeni izole test ediyor; e2e testi ise tarayıcıda gerçek uygulamayı açıp gerçek API'den gerçek veritabanına kadar uzanan yolu doğruluyor. Playwright'ın webServer ayarı test başlamadan önce birden fazla süreci ayağa kaldırıp hazır olmalarını bekleyebiliyor:

import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env['CI'];

export default defineConfig({
  testDir: './e2e',
  forbidOnly: isCI,
  retries: isCI ? 1 : 0,
  reporter: isCI ? [['github'], ['html', { open: 'never' }]] : 'list',
  use: {
    baseURL: 'http://localhost:4200',
    trace: 'on-first-retry',
  },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: [
    {
      command: 'dotnet run --project ../api/src/Bookstore.Api --urls http://localhost:5080',
      url: 'http://localhost:5080/health',
      reuseExistingServer: !isCI,
      timeout: 180_000,
    },
    {
      command: 'npm run start -- --port 4200',
      url: 'http://localhost:4200',
      reuseExistingServer: !isCI,
      timeout: 180_000,
    },
  ],
});
import { expect, test } from '@playwright/test';

test('lists the catalog from the API', async ({ page }) => {
  await page.goto('/');

  const books = page.getByTestId('book');
  await expect(books).toHaveCount(3);
  await expect(books.filter({ hasText: 'Clean Architecture' })).toContainText('Sold out');
});

İlk server API'yi dotnet run ile 5080'de başlatıyor ve /health 200 dönene kadar bekliyor; ikincisi Angular dev server'ını başlatıyor, proxy.conf.json sayesinde /api istekleri 5080'e gidiyor. API'nin ihtiyaç duyduğu veritabanı workflow'daki service container. forbidOnly CI'da yanlışlıkla commit'lenmiş bir test.only'yi hata sayıyor; retries: 1 ve trace: 'on-first-retry' ikilisi, ilk denemede kırılan testin ikinci denemesinde bir trace dosyası kaydediyor. github reporter'ı hataları GitHub'da annotation olarak gösteriyor.

  e2e:
    needs: lint-test-build
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:17-alpine
        env:
          POSTGRES_DB: bookstore
          POSTGRES_USER: bookstore
          POSTGRES_HOST_AUTH_METHOD: trust
        ports:
          - 18232:5432
        options: >-
          --health-cmd "pg_isready -U bookstore -d bookstore"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
    env:
      ConnectionStrings__Bookstore: Host=localhost;Port=18232;Database=bookstore;Username=bookstore
      Database__EnsureCreated: 'true'
    defaults:
      run:
        working-directory: web
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          global-json-file: global.json
      - uses: actions/setup-node@v7
        with:
          node-version-file: web/package.json
          cache: npm
          cache-dependency-path: web/package-lock.json
      - run: npm ci
      - uses: actions/cache@v6
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('web/package-lock.json') }}
      - run: npx playwright install --with-deps chromium
      - run: npm run e2e
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: |
            web/playwright-report/
            web/test-results/
          retention-days: 14

Playwright'ın tarayıcıları npm paketiyle gelmiyor, npx playwright install ile ayrıca indiriliyor. Bunları ~/.cache/ms-playwright altında actions/cache@v6 ile cache'liyoruz; key'de lock dosyasının hash'i var, çünkü Playwright sürümü değişince tarayıcı sürümü de değişir. --with-deps tarayıcının ihtiyaç duyduğu sistem paketlerini de kuruyor; bu kısım cache'lenmiyor, her job'da apt ile tekrar kurulur. Rapor ve trace'ler !cancelled() ile her durumda artifact olarak yükleniyor: e2e kırıldığında ilk bakacağın yer HTML (HyperText Markup Language) raporu ve trace viewer.

[web-ci/e2e            ] container health of 87773c5bb9f83c0d3a2619c12829f14cc13998e7f7bfdcc9c8049a08a57a6625 (postgres:17-alpine) is healthy
...
[web-ci/e2e            ]   | Cache not found for input keys: playwright-Linux-ARM64-ae784366ea1f37547f023535ee13d80784f121cd646ec659bcc63f8118c01ad2
[web-ci/e2e            ]   | Chrome Headless Shell 158.0.8127.18 (playwright chromium-headless-shell v1254) downloaded to /root/.cache/ms-playwright/chromium_headless_shell-1254
[web-ci/e2e            ]   ✅  Success - Main npx playwright install --with-deps chromium [32.616624375s]
[web-ci/e2e            ] ⭐ Run Main npm run e2e
[web-ci/e2e            ]   | Running 1 test using 1 worker
[web-ci/e2e            ]   |   1 passed (1.9m)
[web-ci/e2e            ]   ❓  ::notice title=🎭 Playwright Run Summary::  1 passed (1.9m)
[web-ci/e2e            ]   | Artifact playwright-report has been successfully uploaded! Final size is 194377 bytes. Artifact ID is 61284767
[web-ci/e2e            ] ⭐ Run Post actions/cache@v6
[web-ci/e2e            ]   | Cache Size: ~292 MB (306192637 B)
[web-ci/e2e            ]   | Cache saved with key: playwright-Linux-ARM64-ae784366ea1f37547f023535ee13d80784f121cd646ec659bcc63f8118c01ad2

Bu ilk koşuda tarayıcı cache'i yoktu; Playwright Chrome for Testing 158'i indirdi ve kurulum 33 saniye sürdü. Testin kendisi 1,9 dakika gösteriyor, çünkü bu süreye webServer'ların başlaması da dahil: API'nin ilk dotnet run'ı projeyi sıfırdan derledi, Angular dev server'ı da ilk build'ini yaptı. 292 MB'lık tarayıcı cache'i job sonunda kaydedildi. Artifact adımlarını act'te upload-artifact@v4 ile koşturduk; act 0.2.97'nin artifact server'ı @v7'yi desteklemiyor (ayrıntısı bir önceki yazıda).

Üretimde Dikkat

e2e testleri pahalı ve kırılgan; azını, kritik akışlar için yaz. Bookstore'da "katalog açılıyor ve stok durumu doğru görünüyor" tek bir test; sepet ve ödeme akışı eklendiğinde her biri birer test olur. Her UI (User Interface) değişikliğini e2e ile test etmeye kalkarsan pull request'ler dakikalarca bekler ve testler sık sık rastgele kırılır.

Cache'in etkisini aynı workflow'u ikinci kez koşturarak gördük: Playwright cache'i geri yüklendi (Cache restored from key: playwright-linux-arm64-…), npx playwright install --with-deps 33 saniyeden 15 saniyeye indi (sistem paketleri yine apt ile kuruldu), test 1 passed (10.3s) ile bitti ve bütün workflow 4,5 dakikadan 2 dakikaya düştü. İlk koşudaki 1,9 dakikanın büyük kısmı makinenin o sıradaki yükü ve soğuk build'lerdi; GitHub-hosted runner'da sayılar farklı olacaktır, ama oran aynı yöne gider.

Flaky (bazen geçip bazen kalan) bir e2e testini retries ile gizlemek cazip, ama Playwright'ın raporu "flaky" olarak işaretlenen testleri ayrıca listeliyor. O listeyi takip et; aynı test her hafta orada görünüyorsa sorun testte ya da uygulamada, retry'da değil.

Sık Düşülen Tuzaklar

Lock dosyası drift'i. package.json'ı elle düzenleyip lock dosyasını commit'lememek npm ci'ı durdurur. npm install sonrası iki dosyayı birlikte commit'le.

Node major uyumsuzluğu. Workflow'da node-version: 20 gibi sabit bir değer, Angular yükseltildiğinde CLI'ın hiç açılmamasına yol açar. Sürümü package.json'dan oku.

Watch modunda takılan test job'ı. TTY'li bir ortamda ng test ya da doğrudan vitest watch moduna girip asla bitmez; job da varsayılan 360 dakikalık timeout'a kadar bekler. --watch=false ve timeout-minutes birlikte.

working-directory'nin uses: step'lerine uygulandığını sanmak. defaults.run yalnız run: step'lerini etkiler; action input'larındaki yollar repository köküne göre yazılır.

Monorepo'da otomatik npm cache'ine güvenmek. setup-node, packageManager alanını yalnız repository kökündeki package.json'da arıyor; uygulama bir alt klasördeyse cache: npm ve cache-dependency-path'i açıkça yaz.

e2e'yi health check'siz veritabanıyla koşturmak. Bu yazıyı hazırlarken Playwright testini yerelde ilk kez çalıştırdığımızda PostgreSQL container'ı henüz hazır değildi ve API, EnsureCreated sırasında düştü: Process from config.webServer was not able to start. Exit code: 134. İkinci denemede geçti. CI'da aynı yarışı service container'ın health check'i engelliyor.

Küçük Bir Deneme

★☆☆ angular.json'daki anyComponentStyle budget'ını 1 kB'a indir, bir component'in CSS'ine (Cascading Style Sheets) uzun bir kural ekle ve ng build'in ne söylediğine bak.

★★☆ web-ci.yml'a Vitest coverage ekle (ng test --coverage) ve coverage özetini job summary'ye yaz.

★★★ e2e'ye "kitap detayı" senaryosu ekle: API'de olmayan bir kitap id'si için uygulamanın ne gösterdiğini test et. Testi bilerek kır, act ile çalıştır ve playwright-report artifact'ındaki trace'i npx playwright show-trace ile aç.

Şu an iki ayrı workflow'umuz var: api-ci.yml ve web-ci.yml, her biri kendi path filtresiyle. Bir sonraki yazıda monorepo'nun asıl sorularına geçiyoruz: ayrı workflow mu tek workflow mu, required check'ler path filtreleriyle nasıl uyumlu olur, Angular'ın dist'i .NET publish'e nasıl taşınır ve tag push'unda tek bir release paketi nasıl üretilir.