Mert Özen Her Satırda Daha İleri
DevOps Jenkins — #08

Jenkins ile Angular Pipeline: Node Container, npm Cache, Headless Test ve stash/unstash

Mert Özen 8 Kas 2026 9 dk 4 görüntülenme
Jenkins ile Angular Pipeline: Node Container, npm Cache, Headless Test ve stash/unstash

Angular 22 uygulamasını node:24 container'ında npm ci, ng lint, Vitest ve ng build ile .NET API'yle paralel build edip stash/unstash ile tek pakette birleştiriyoruz. npm cache volume'ü, Vitest'in JUnit reporter'ı, budget'lar, browser modu için eksik paket hatası, 329 MB'lık node_modules'ü stash etmemek ve Pipeline Graph View'da paralel stage süreleri.

Sürüm notu: Bu yazıdaki örnekler Jenkins 2.580.2 LTS (Long-Term Support), Node.js 24 (node:24-trixie image'ı, npm 11.19), Angular 22.2, Vitest 5.1, angular-eslint ve .NET 10 ile denenmiştir.

Önce Tahmin Et

Angular build'i bir agent'ta, .NET build'i başka bir agent'ta koşuyor ve ikisinin çıktısını üçüncü bir stage'de tek pakette birleştirmek istiyorsun. Bir arkadaşın "kolay, Web stage'inin sonunda bütün bookstore-web klasörünü stash edelim" diyor. O klasörde 220 KB'lık bir dist ve bir de node_modules var. Sence node_modules kaç megabayt ve stash edilen her byte nereye gidiyor?

Paralel İki Uygulama, Tek Paket

Bookstore'un Jenkinsfile'ı artık iki uygulamayı birlikte build ediyor. API (Application Programming Interface) ve Web stage'leri paralel koşuyor, ardından Package stage'i ikisinin çıktısını birleştiriyor:

pipeline {
    agent none
    options {
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
        buildDiscarder(logRotator(numToKeepStr: '20'))
        disableConcurrentBuilds()
    }
    environment {
        VERSION = "1.4.${BUILD_NUMBER}"
    }
    stages {
        stage('Build and Test') {
            parallel {
                stage('API') {
                    agent {
                        docker {
                            image 'mcr.microsoft.com/dotnet/sdk:10.0'
                            label 'docker'
                            args '-v bookstore-nuget:/nuget'
                        }
                    }
                    environment {
                        NUGET_PACKAGES = '/nuget/packages'
                        DOTNET_CLI_TELEMETRY_OPTOUT = '1'
                        DOTNET_NOLOGO = '1'
                    }
                    steps {
                        sh 'dotnet restore Bookstore.slnx'
                        sh 'dotnet build Bookstore.slnx -c Release --no-restore -p:Version=$VERSION'
                        sh '''
                            dotnet test --solution Bookstore.slnx -c Release --no-build \
                                --results-directory TestResults \
                                --report-xunit-junit --report-xunit-junit-filename bookstore-api.junit.xml \
                                --coverage --coverage-output-format cobertura --coverage-output bookstore-api.cobertura.xml
                        '''
                        sh 'dotnet publish src/Bookstore.Api -c Release --no-build -p:Version=$VERSION -o out/api'
                        stash name: 'api-publish', includes: 'out/api/**'
                    }
                    post {
                        always {
                            junit testResults: 'TestResults/**/*.junit.xml'
                            recordCoverage(tools: [[parser: 'COBERTURA', pattern: 'TestResults/**/*.cobertura.xml']],
                                           sourceCodeRetention: 'NEVER',
                                           qualityGates: [[threshold: 80.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE']])
                        }
                    }
                }
                stage('Web') {
                    agent {
                        docker {
                            image 'node:24-trixie'
                            label 'docker'
                            args '-v bookstore-npm:/npm'
                        }
                    }
                    environment {
                        npm_config_cache = '/npm'
                        NG_CLI_ANALYTICS = 'false'
                    }
                    steps {
                        dir('bookstore-web') {
                            sh 'npm ci --no-audit --no-fund'
                            sh 'npx ng lint'
                            sh 'npx ng test --watch=false --reporters=default --reporters=junit --output-file=test-results/web.junit.xml'
                            sh 'npx ng build'
                            sh 'du -sh node_modules dist/bookstore-web/browser'
                        }
                        stash name: 'web-dist', includes: 'bookstore-web/dist/bookstore-web/browser/**'
                    }
                    post {
                        always {
                            junit testResults: 'bookstore-web/test-results/*.junit.xml'
                        }
                    }
                }
            }
        }
        stage('Package') {
            agent { label 'linux' }
            steps {
                unstash 'api-publish'
                unstash 'web-dist'
                sh '''
                    mkdir -p out/api/wwwroot
                    cp -r bookstore-web/dist/bookstore-web/browser/. out/api/wwwroot/
                    tar -czf bookstore-$VERSION.tgz -C out/api .
                    tar -tzf bookstore-$VERSION.tgz | grep wwwroot | head -5
                '''
                archiveArtifacts artifacts: "bookstore-${VERSION}.tgz", fingerprint: true
            }
            post {
                cleanup {
                    cleanWs()
                }
            }
        }
    }
}

Üst seviyede agent none var: hiçbir executor pipeline boyunca boşuna tutulmuyor, her stage ihtiyacı olan agent'ı kendisi alıyor. API stage'i bir önceki yazıdakinin aynısı, yalnız artık arşiv yerine stash ile bitiyor. Uygulamanın Angular build'ini de servis edebilmesi için Program.cs'e üç satır ekledim: app.UseDefaultFiles(), app.UseStaticFiles() ve route'ların sonunda app.MapFallbackToFile("index.html"). Böylece /api/books API'ye, geri kalan her adres Angular'ın index.html'ine gidiyor; SPA (Single Page Application) routing'inin sunucu tarafını Angular serisinin "Angular Build ve Deploy: ng build, Environment Dosyaları, Budgets ve SPA Routing" yazısında anlatmıştık.

Node Container'ı ve npm Cache'i

Web stage'i resmi node:24-trixie image'ında koşuyor. Angular 22'nin istediği Node sürümü ^22.22.3 || ^24.15.0 || >=26.0.0; agent'a Node kurmuyoruz, image tag'i bunu belirliyor. .NET tarafındaki NuGet cache'inin karşılığı burada npm cache'i: npm_config_cache=/npm ve kalıcı bir bookstore-npm volume'ü. Node image'ında uid 1000 node kullanıcısı, volume'ü de aynı şekilde bir kez hazırladım:

$ docker run --rm -u 0 -v bookstore-npm:/npm node:24-trixie sh -c 'chown 1000:1000 /npm && ls -ld /npm'
drwxr-xr-x 2 node node 4096 Nov  8 04:37 /npm

Cache'i node_modules ile karıştırma. npm ci her seferinde node_modules'ü silip package-lock.json'dan birebir yeniden kurar; cache ise indirilen paket arşivlerini tutar, ağdan tekrar indirmeyi önler. node_modules'ü build'ler arasında saklamaya çalışmak (workspace'te bırakmak ya da cache volume'üne koymak) lock dosyası değiştiğinde tutarsız kurulumlara yol açar; npm ci + cache doğru ikili. İlk build'in log'u:

[2026-11-08T04:37:36.196Z] + npm ci --no-audit --no-fund
[2026-11-08T04:37:42.722Z] added 419 packages in 6s
[2026-11-08T04:37:42.722Z] npm warn install-scripts 4 packages have install scripts not yet covered by allowScripts:
[2026-11-08T04:37:42.722Z] npm warn install-scripts   @parcel/watcher@2.6.0 (install: node scripts/build-from-source.js)
[2026-11-08T04:37:42.722Z] npm warn install-scripts   esbuild@0.28.2 (postinstall: node install.js)
[2026-11-08T04:37:42.722Z] npm warn install-scripts   lmdb@3.5.6 (install: node-gyp-build-optional-packages)
[2026-11-08T04:37:42.722Z] npm warn install-scripts   msgpackr-extract@3.0.7 (install: node-gyp-build-optional-packages)

Uyarı satırları npm'in bu sürümünün install script'i çalıştıran paketleri listelemesi: esbuild gibi paketler kurulumda platforma uygun ikiliyi indiriyor. Bu satırları yok sayma; supply chain açısından hangi paketin kurulumda kod çalıştırdığını bilmek değerli. Konunun güvenlik tarafını Web Güvenliği serisinin "OWASP (Open Worldwide Application Security Project) A03: Software Supply Chain Failures" yazısında konuşmuştuk.

Lint, Test ve Build

Angular 22 projesi ng new ile lint yapılandırması olmadan geliyor; ng add angular-eslint ile ekledim ve ng lint komutu oluştu. Test tarafında Angular 22'nin varsayılanı Vitest. ng test tarayıcı seçilmezse testleri Node.js içinde, jsdom ile koşturuyor; yani CI (Continuous Integration) container'ında tarayıcı gerekmiyor. Jenkins'in okuyacağı JUnit raporu için Vitest'in yerleşik junit reporter'ını açtım:

[2026-11-08T04:37:42.991Z] + npx ng lint
[2026-11-08T04:37:43.801Z] All files pass linting.
[2026-11-08T04:37:44.871Z] + npx ng test --watch=false --reporters=default --reporters=junit --output-file=test-results/web.junit.xml
[2026-11-08T04:37:46.508Z] + npx ng build
[2026-11-08T04:37:47.320Z] Initial chunk files | Names         |  Raw size | Estimated transfer size
[2026-11-08T04:37:47.320Z] main-RWCIUYT4.js    | main          | 198.68 kB |                54.52 kB
[2026-11-08T04:37:47.320Z] styles-5INURTSO.css | styles        |   0 bytes |                 0 bytes
[2026-11-08T04:37:47.320Z] Application bundle generation complete. [0.730 seconds] - 2026-11-08T04:37:47.253Z
[2026-11-08T04:37:47.320Z] Output location: /home/jenkins/agent/workspace/bookstore_main@2/bookstore-web/dist/bookstore-web

--watch=false şart: watch modunda ng test hiç bitmez ve build timeout'a kadar asılı kalır. JUnit raporunun başı, Jenkins'in beklediği biçimde:

<testsuites name="vitest tests" tests="4" failures="0" errors="0" time="0.025574126">
    <testsuite name="src/app/app.spec.ts" timestamp="2026-11-08T04:37:02.745Z" hostname="eff54429eedc" tests="2" failures="0" errors="0" skipped="0" time="0.023815459">

Build'in kendisinde budget'lar devrede: angular.json'daki production yapılandırması initial bundle için 500 kB'ta uyarı, 1 MB'ta hata veriyor. Budget aşılırsa ng build sıfırdan farklı bir exit code döner ve stage kırmızıya döner; yani budget'lar ayrı bir araç gerektirmeden pipeline'ın quality gate'i olur. İki uygulamanın testleri aynı build'in raporunda birleşiyor:

{"_class":"hudson.tasks.junit.TestResult","failCount":0,"passCount":10,"suites":[{"name":"_home_jenkins_agent_workspace_bookstore_main_tests_Bookstore.Api.Tests_bin_Release_net10.0_Bookstore.Api.Tests.dll"},{"name":"src_app_app.spec.ts"},{"name":"src_app_book-price.spec.ts"}]}

Headless Tarayıcı Gerektiğinde

Testlerin gerçek bir tarayıcıda koşmasını istersen (--browsers=ChromiumHeadless), Vitest'in browser modu ve bir sağlayıcı paketi gerekir. node:24-trixie image'ında, paketleri eklemeden denediğimde:

The following packages are required but were not found:
  - The "browsers" option requires either "@vitest/browser-playwright", "@vitest/browser-webdriverio", or "@vitest/browser-preview" to be installed.
Please install the missing packages and rerun the test command.

Paketi eklemek de yetmez; Chromium'un kendisi ve onun sistem kütüphaneleri de image'da olmalı. Bunun için ya Playwright'ın tarayıcıları ve bağımlılıkları kurulu resmi image'ını agent olarak kullanırsın ya da kendi image'ında npx playwright install --with-deps chromium çalıştırırsın. Bu yazıda browser modunu çalıştırmadım; Bookstore'un testleri jsdom ile yeterli.

stash ve unstash

Paralel stage'ler aynı agent'a düşse bile ayrı workspace'lerde koşuyor; log'da Web stage'inin bookstore_main@2 klasöründe çalıştığını görüyorsun. Package stage'i ise bu çalıştırmada başka bir agent'a, ssh-agent-1'e düştü. Workspace'ler arasında dosya taşımanın Jenkins'teki yolu stash/unstash: dosyalar agent'tan controller'a kopyalanır, build boyunca orada saklanır ve unstash edildiği agent'a geri kopyalanır.

[2026-11-08T04:37:43.373Z] Stashed 9 file(s)
[2026-11-08T04:37:47.588Z] + du -sh node_modules dist/bookstore-web/browser
[2026-11-08T04:37:47.588Z] 329M	node_modules
[2026-11-08T04:37:47.588Z] 220K	dist/bookstore-web/browser
[2026-11-08T04:37:47.628Z] Stashed 4 file(s)
[2026-11-08T04:37:48.937Z] Running on ssh-agent-1 in /home/jenkins/agent/workspace/bookstore_main
[2026-11-08T04:37:49.382Z] ./wwwroot/
[2026-11-08T04:37:49.382Z] ./wwwroot/favicon.ico
[2026-11-08T04:37:49.382Z] ./wwwroot/styles-5INURTSO.css
[2026-11-08T04:37:49.382Z] ./wwwroot/main-RWCIUYT4.js
[2026-11-08T04:37:49.382Z] ./wwwroot/index.html

Baştaki sorunun cevabı: node_modules 329 MB, dist 220 KB. Bütün bookstore-web klasörünü stash etmek her build'de 329 MB'ı agent'tan controller'a, oradan diğer agent'a taşımak demek; controller'ın ağı ve diski bunu her build'de öder. Stash'i küçük ve ara dosyalar için kullan (includes ile tam olarak gereken klasör); büyük ve kalıcı çıktılar için artifact repository daha doğru yer.

Pipeline Graph View'da Paralel Stage'leri Okumak

Pipeline Graph View plugin'i build sayfasında stage'leri bir grafik olarak gösteriyor: paralel dallar yan yana, her birinin süresi ve durumu ayrı. Aynı verinin plugin'in kullandığı endpoint'ten (/job/bookstore/job/main/4/stages/tree) çıkardığım özeti:

Build and Test: success, STAGE, 13890 ms
  API: success, PARALLEL, 9625 ms
  Web: success, PARALLEL, 13860 ms
Package: success, STAGE, 573 ms

Ardışık koşsalardı API ve Web toplam yaklaşık 23,5 saniye sürecekti; paralelde "Build and Test" 13,9 saniye, yani en yavaş dal kadar. Grafikte bunu bir bakışta görürsün: kritik yol Web dalı, hızlandırmak istiyorsan önce oraya bakmalısın. Blue Ocean'ın yerini alan bu görünüme son yazıda tekrar döneceğiz.

Üretimde Dikkat

package-lock.json repository'de olmalı ve npm ci kullanılmalı. npm install lock dosyasını güncelleyebilir; CI'da bu, test edilen bağımlılıklarla repository'deki bağımlılıkların ayrışması demek.

Ortam ayarını build'e gömme. Angular'ın environment dosyaları değerleri build sırasında bundle'ın içine yazar; staging için ayrı, production için ayrı build alırsan test ettiğin paketle yayına çıkan paket farklı olur. Bookstore'da web uygulaması API ile aynı origin'den servis edildiği için API'yi göreli /api adresiyle çağırabilir; böylece ortamın adresi bundle'a hiç girmez ve tek bir build her ortamda çalışır. Ortama göre değişmesi gereken değerler varsa (ör. bir analytics anahtarı), onları açılışta /config.json gibi bir dosyadan okuyan bir runtime configuration yaklaşımı "build once, deploy many" ilkesini korur. Bu pipeline'ın ürettiği tek bookstore-1.4.4.tgz, dokuzuncu yazıda önce staging'e sonra production'a aynen gidecek.

Image tag'ini sabitle. node:24-trixie Node 24'ün her yeni sürümüyle kayar; tamamen tekrarlanabilir build için tam sürümlü tag ya da digest kullan.

Paralel dalların aynı agent'a düşmesi kaynak rekabeti yaratır. Bizim ortamda docker-agent-1'in iki executor'ı vardı ve iki dal aynı anda aynı makinede koştu. Ağır build'lerde label ile dalları farklı agent'lara dağıtmayı ya da executor sayısını düşük tutmayı düşün.

Paralel bir dal başarısız olursa diğeri varsayılan olarak çalışmaya devam eder. Hızlı geri bildirim istiyorsan options { parallelsAlwaysFailFast() } ya da parallel bloğunda failFast true kullan; dokuzuncu yazıda bunu deneyeceğiz.

Sık Düşülen Tuzaklar

node_modules'ü stash etmek. Yüzlerce megabayt her build'de controller üzerinden taşınır.

ng test'i watch modunda bırakmak. Pipeline timeout'a kadar asılı kalır; --watch=false unutulmamalı.

Tarayıcı bağımlılıkları eksik image. Browser modunda test koşacaksan Chromium'u ve sistem kütüphanelerini içeren bir image seç; yoksa yukarıdaki hatayı ya da daha kötüsü, eksik bir .so dosyası yüzünden açılamayan bir tarayıcıyı görürsün.

Cache volume'ünün sahipliği. Root'a ait bir volume, uid 1000 ile çalışan npm'e EACCES hatası verir.

Budget uyarılarını görmezden gelmek. Uyarı eşiği build'i kırmaz; aynı uyarıyı aylarca görmek, hata eşiğine sessizce yaklaştığın anlamına gelir.

Küçük Bir Deneme

★☆☆ Web stage'inde bilerek bir lint hatası yap (ör. kullanılmayan bir import) ve ng lint'in stage'i nasıl kırdığını gör.

★★☆ angular.json'daki initial budget hata eşiğini mevcut bundle'ın altına çek, build'i çalıştır ve hatanın log'daki hâlini incele. Sonra eşiği geri al.

★★★ Stash'i bilerek bütün bookstore-web klasörüne genişlet ve iki build'in sürelerini karşılaştır. Sonra stash yerine API ve Web'i aynı stage'de sırayla build eden bir alternatif yaz; hangisi senin ortamında daha hızlı?

Paket hazır ama henüz bir yere gitmedi. Bir sonraki yazıda deploy'a geçiyoruz: parallel ve failFast, matrix, input ile onay, Lockable Resources ile eşzamanlı deploy'u engellemek ve bir Linux sunucuya sshagent, rsync ve systemctl ile deploy.