Mert Özen Her Satırda Daha İleri
DevOps Azure DevOps Pipelines — #04

Azure Pipelines ile .NET + Angular CI: UseDotNet, NuGet Cache, Test Sonuçları, Coverage ve Pipeline Artifact

Mert Özen 15 Kas 2026 12 dk 6 görüntülenme
Azure Pipelines ile .NET + Angular CI: UseDotNet, NuGet Cache, Test Sonuçları, Coverage ve Pipeline Artifact

Azure Pipelines'ta .NET ve Angular CI'ı, lock dosyasına bağlı Cache@2, TRX ve JUnit için PublishTestResults@2, Cobertura için PublishCodeCoverageResults@2 ve job'lar arası PublishPipelineArtifact@1 ile kurulur. Bookstore'un template'lerine bunları ekliyor, DotNetCoreCLI@2'nin Microsoft.Testing.Platform'da --report-trx eklemesini, --locked-mode'u ve api ile web çıktısını tek release paketinde birleştiren job'ı görüyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), Microsoft.Testing.Extensions.TrxReport 2.5.3, Microsoft.Testing.Extensions.CodeCoverage 18.12.0, Angular 22, Vitest 5.1 ve Node.js 24 ile yerelde denenmiştir; pipeline ve template dosyaları yq 4.54 ile ayrıştırılarak denetlenmiştir. Cache, test sonucu, coverage ve artifact task'larının Azure Pipelines içindeki davranışı için Microsoft Learn'deki resmî belgelerden (Kasım 2026 sürümleri) yararlanılmıştır.

Önce Tahmin Et

Emre, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) pipeline'ındaki test step'ini düz bir script yerine hazır task'la yazmak istiyor:

- task: DotNetCoreCLI@2
  displayName: Test
  inputs:
    command: test
    arguments: --solution Bookstore.slnx --no-build -c Release

Repository'nin global.json'u test runner olarak Microsoft.Testing.Platform'u seçiyor ve test projesi yalnızca xUnit v3 paketine sahip. Task'ın publishTestResults input'u yazılmadı. Testlerin hepsi geçecek durumda. Step yeşil mi biter?

Cevap: kırmızı biter ve tek bir test bile koşmaz. DotNetCoreCLI@2'nin belgesine göre publishTestResults'ın varsayılan değeri true ve global.json'da Microsoft.Testing.Platform görünce komuta --report-trx --results-directory $(Agent.TempDirectory) ekliyor. Ama --report-trx seçeneği ayrı bir pakette, Microsoft.Testing.Extensions.TrxReport'ta. Task'ın eklediği seçenekle aynı komutu yerelde, aynı repository'de çalıştırınca sonuç şu (yolları kısalttım):

$ dotnet test --project tests/Bookstore.Api.Tests --no-build -c Release --report-trx --results-directory …/tr
Test run summary: Failed!
  total: 0
  failed: 0
  succeeded: 0
  skipped: 0
  duration: 73ms
Test run completed with non-success exit code: 5 (see: https://aka.ms/testingplatform/exitcodes)

Handshake failures:
  …/tests/Bookstore.Api.Tests/bin/Release/net10.0/Bookstore.Api.Tests.dll (net10.0)
Exit code: 5
  Standard output: Unknown option '--report-trx'
  Option '--report-trx' is provided by the 'Microsoft.Testing.Extensions.TrxReport' extension. Add a package reference to use it.

Hata mesajı çözümü de söylüyor. Bu yazıda o paketi ekleyip test sonuçlarını, coverage'ı, cache'i ve pipeline artifact'larını Bookstore'un ortak template'lerine yerleştireceğiz.

DotNetCoreCLI@2 mi, Düz dotnet Komutu mu

SDK ve Node.js sürümünü önceki yazılardaki gibi sabitliyoruz: UseDotNet@2 useGlobalJson: true ile global.json'u, UseNode@1 ise version: 24.x ile Node.js 24'ü kuruyor. Asıl karar, komutları task ile mi yoksa script ile mi çalıştıracağın. DotNetCoreCLI@2 bazı işleri kendiliğinden yapıyor: test sonucunu TRX (Visual Studio'nun XML tabanlı test sonucu biçimi) olarak üretip yayınlıyor, birden fazla projeyi glob ile bulabiliyor, feed kimlik bilgilerini restore'a geçirebiliyor. Bedeli, komutun bir kısmının senin görmediğin yerde oluşması; giriş bölümündeki hata tam olarak bu. Düz script: dotnet ... ise yerelde yazdığın komutun aynısı: log'da ne çalıştığı açık, yerelde birebir tekrarlanabiliyor, test sonucunu yayınlamak için ayrı bir task gerekiyor.

Bookstore'da düz komutları seçtik ve yayınlamayı ayrı task'lara bıraktık. Bunun ön koşulu, testlerin rapor ve coverage üretebilmesi; test projesine iki Microsoft.Testing.Platform eklentisi ekledik (sürümler Directory.Packages.props'ta):

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
    <PackageReference Include="xunit.v3" />
    <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
    <PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
</ItemGroup>

Paketler eklendikten sonra test uygulamasının yardım çıktısında yeni seçenekler görünüyor:

$ dotnet test --project tests/Bookstore.Api.Tests --no-build -c Release --help
…
  --coverage                        Collect the code coverage using dotnet-coverage tool.
  --coverage-output                 The output of the generated code coverage report.
  --coverage-output-format          Output file format. Supported values: 'coverage', 'xml' and 'cobertura'
…
  --report-trx                      Enable generating TRX report.
  --report-trx-filename             The name of the generated TRX report. May include a relative or absolute path; relative paths are resolved against the test results directory and missing directories are created.

NuGet ve npm İçin Cache@2

Microsoft-hosted agent'ta her job temiz bir VM'de başladığı için her restore bütün NuGet paketlerini ve her npm ci bütün npm paketlerini yeniden indiriyor. Cache@2 bir klasörü bir anahtarla saklayıp sonraki run'larda geri yüklüyor. Anahtarın değişmesi gereken an, bağımlılıkların değiştiği an; bunu en iyi lock dosyaları söylüyor. npm'de package-lock.json zaten var. NuGet tarafında lock dosyasını açmak için Directory.Build.props'a <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> ekledik; ilk restore her proje için bir packages.lock.json üretiyor:

{
  "version": 2,
  "dependencies": {
    "net10.0": {
      "Microsoft.AspNetCore.Mvc.Testing": {
        "type": "Direct",
        "requested": "[10.0.15, )",
        "resolved": "10.0.15",
        "contentHash": "qwZzLn60Ish1cTZoNDcbS/P8zjUjLGtZUye18KwB8n05eea5WGBgYO/HAxZWLRcFVkSD2ZXY496e1YX7Go4ndQ==",

Ortak step template'i artık cache'i geri yükleyip restore'u kilitli modda çalıştırıyor:

# global.json'daki SDK'yı kurar, NuGet cache'ini geri yükler ve solution'ı kilitli modda restore eder.
parameters:
  - name: solution
    type: string

steps:
  - task: UseDotNet@2
    displayName: Install .NET SDK from global.json
    inputs:
      packageType: sdk
      useGlobalJson: true

  - task: Cache@2
    displayName: Cache NuGet packages
    inputs:
      key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/**/packages.lock.json'
      restoreKeys: |
        nuget | "$(Agent.OS)"
      path: $(NUGET_PACKAGES)

  - script: dotnet restore ${{ parameters.solution }} --locked-mode
    displayName: Restore (locked)

Anahtar üç parçadan oluşuyor: sabit bir metin, agent'ın işletim sistemi ve lock dosyalarının içeriğinden hesaplanan hash. Belgelere göre dosya yolu verilen parça, eşleşen bütün dosyaların içeriğinden hash'leniyor. restoreKeys tam eşleşme yoksa denenecek önekleri veriyor; lock dosyası değişmişse aynı işletim sisteminin en son cache'i yükleniyor ve restore yalnızca farkı indiriyor. Cache'in nereye yazılacağını NUGET_PACKAGES değişkeni belirliyor; ortak variable template'ine NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages ve npm_config_cache: $(Pipeline.Workspace)/.npm satırlarını ekledik, ikisi de step'lere environment variable olarak gidiyor. Kaydetme job'ın sonunda, Azure Pipelines'ın kendiliğinden eklediği "Post-job: Cache" adımında oluyor ve yalnızca job başarılıysa yapılıyor.

--locked-mode, lock dosyasıyla proje dosyası uyuşmadığında restore'u durduruyor. Bir geliştiricinin Directory.Packages.props'ta sürümü değiştirip lock dosyasını commit'lemeyi unuttuğu durumu yerelde canlandırınca (yolu kısalttım):

$ dotnet restore Bookstore.slnx --locked-mode
  Determining projects to restore...
…/tests/Bookstore.Api.Tests/Bookstore.Api.Tests.csproj : error NU1004: The package reference xunit.v3 version has changed from [4.0.3, ) to [4.0.2, ).The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. Disable the RestoreLockedMode MSBuild property or pass an explicit --force-evaluate option to run restore to update the lock file. […]
  Failed to restore …/tests/Bookstore.Api.Tests/Bookstore.Api.Tests.csproj (in 24 ms).

Bu hata istediğimiz şey: CI, lock dosyasında yazanın dışında hiçbir paketi çözmüyor ve cache anahtarı her zaman gerçekten kullanılan paketleri temsil ediyor.

Belgelerdeki üç kural cache'in güvenliğini belirliyor. Cache'ler değişmez: bir anahtarla bir kez yazılan içerik güncellenmiyor, içeriği değiştirmek için anahtarı değiştirmek gerekiyor. Cache'i elle silmek mümkün değil; anahtara version2 gibi bir metin eklemek eskiyi devre dışı bırakmanın yolu. Cache'ler pipeline ve branch'e göre ayrı scope'larda tutuluyor: pull request run'ları hedef branch'in cache'ini okuyabiliyor ama ona yazamıyor, böylece bir pull request main'in cache'ini zehirleyemiyor. Yedi gün kullanılmayan cache'in süresi doluyor.

Test Sonuçları: TRX ve JUnit

dotnet-ci.yml'daki test step'i artık rapor ve coverage dosyası üretiyor, ardından iki task bunları run'a ekliyor:

          - script: >-
              dotnet test --solution ${{ parameters.solution }} --no-build -c ${{ parameters.buildConfiguration }}
              --results-directory $(testResultsDir)
              --report-trx --report-trx-filename api-tests.trx
              --coverage --coverage-output-format cobertura --coverage-output api.cobertura.xml
            displayName: Test
            env:
              ConnectionStrings__BookstoreDb: $(TestDbConnection)

          - task: PublishTestResults@2
            displayName: Publish test results
            condition: succeededOrFailed()
            inputs:
              testResultsFormat: VSTest
              testResultsFiles: '*.trx'
              searchFolder: $(testResultsDir)
              testRunTitle: API tests

          - task: PublishCodeCoverageResults@2
            displayName: Publish code coverage
            condition: succeededOrFailed()
            inputs:
              summaryFileLocation: $(testResultsDir)/api.cobertura.xml

testResultsDir job seviyesinde $(Agent.TempDirectory)/TestResults olarak tanımlı. >- YAML'ın (girintiyle yapı kuran metin formatı) katlanmış metin biçimi: satırlar tek bir komut satırına birleşiyor, ama dosyada okunaklı kalıyor. En önemli satır condition: succeededOrFailed(). Test step'i kırılınca sonraki step'ler varsayılan olarak atlanıyor; test sonucunu yayınlayan task'ın tam da bu durumda çalışması gerekiyor, yoksa hangi testin kırıldığını yalnızca log'dan okuyabilirsin.

Testler başarısız olsa da raporların üretildiğini yerelde görebiliriz. Aynı komutu bilerek veritabanı olmadan çalıştırdık; bütün testler kırılıyor ama iki dosya yine de yazılıyor:

$ dotnet test --solution Bookstore.slnx --no-build -c Release --results-directory …/TestResults --report-trx --report-trx-filename bookstore-api.trx --coverage --coverage-output-format cobertura --coverage-output bookstore-api.cobertura.xml
…
  In process file artifacts produced:
    - …/TestResults/bookstore-api.cobertura.xml
    - …/TestResults/bookstore-api.trx
Test run summary: Failed!
  total: 10
  failed: 10
  succeeded: 0
  skipped: 0
  duration: 674ms

PublishTestResults@2 TRX için VSTest, JUnit için JUnit biçimini kullanıyor; yayınlanan sonuçlar run'ın Tests sekmesinde test bazında, geçmiş run'larla karşılaştırmalı görünüyor. failTaskOnFailedTests input'unun varsayılanı false: task, testlerin kırıldığını bildirmekle yetiniyor, run'ı kırmızıya çeviren test step'inin kendisi oluyor.

Angular tarafında Vitest'in JUnit reporter'ını kullanıyoruz. Komut uzadığı için package.json'a bir test:ci script'i ekledik:

"test:ci": "ng test --watch=false --reporters=default --reporters=junit --output-file=test-results/junit.xml --coverage --coverage-reporters=cobertura --coverage-reporters=text-summary"
$ npm run test:ci
…
 RUN  v5.1.0 …/web/bookstore-web
      Coverage enabled with v8

 ✓ |bookstore-web| src/app/app.spec.ts (1 test) 29ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

JUNIT report written to …/web/bookstore-web/test-results/junit.xml
 % Coverage report from v8

=============================== Coverage summary ===============================
Statements   : 51.61% ( 32/62 )
Branches     : 56.25% ( 18/32 )
Functions    : 36.36% ( 4/11 )
Lines        : 40.81% ( 20/49 )
================================================================================

angular-ci.yml'daki PublishTestResults@2 bu dosyayı testResultsFormat: JUnit ile yayınlıyor. Böylece Tests sekmesinde API ve web testleri iki ayrı test run'ı olarak yan yana duruyor.

Coverage: Cobertura ve PublishCodeCoverageResults@2

Coverage'ı yayınlamanın güncel yolu PublishCodeCoverageResults@2. Eski v1 task'ının belgesi, v2'nin kullanılması gerektiğini ve v1'in kullanımdan kaldırılacağını söylüyor. İkisi arasındaki farklar YAML'da hemen görünüyor: v1'de zorunlu olan codeCoverageTool: Cobertura | JaCoCo input'u ve reportDirectory v2'de yok; v2 yalnızca summaryFileLocation, isteğe bağlı pathToSources ve failIfCoverageEmpty alıyor. v2 Cobertura dışında .coverage, .lcov ve farklı XML biçimlerini de okuyabiliyor, birden fazla summary dosyasını tek raporda birleştiriyor ve pull request'lerde diff coverage'ı, yani yalnızca değişen satırların coverage'ını hesaplayabiliyor. Belgedeki bir ön koşul dikkat çekici: v2 task'ından önce aynı job'da UseDotNet@2 çalışmış olmalı. Bizim API job'ımızda bu zaten var.

Angular coverage'ı için Vitest'in coverage sağlayıcısı ayrıca kuruluyor. Paket eksikken --coverage ile test çalıştırınca hata açık:

$ npx ng test --watch=false --coverage --coverage-reporters=cobertura --coverage-reporters=text-summary
The following packages are required but were not found:
  - Code coverage requires either "@vitest/coverage-v8" or "@vitest/coverage-istanbul" to be installed.
Please install the missing packages and rerun the test command.

npm install --save-dev @vitest/coverage-v8@5.1.0 ile Vitest'in sürümüyle aynı sürümü ekledik. Angular coverage'ı şimdilik log'daki özet satırlarıyla sınırlı; iki job'ın Cobertura dosyalarını bir sonraki job'da birleştirip tek coverage sekmesi oluşturmak yazının sonundaki denemelerden biri.

Paralel Job'lar ve Pipeline Artifact ile Tek Release Paketi

İskeletin Build stage'i artık beş job'dan oluşuyor:

04-build-flow-tr.png

Api ve Web yalnızca Version'a bağlı, birbirlerine değil; organization'da iki boş parallel job varsa aynı anda koşuyorlar. Her biri çıktısını PublishPipelineArtifact@1 ile bir pipeline artifact olarak yüklüyor: dotnet publish çıktısı api, Angular'ın dist/bookstore-web/browser klasörü web adıyla. Cache ile artifact'ı karıştırmamak gerekiyor; belgeler ayrımı net yapıyor: artifact, bir job'ın ürettiği ve başka bir job'ın onsuz çalışamayacağı dosyalar için; cache ise yalnızca hızlandırmak için, yoksa da iş yürümeli.

Package job'ı dört job'ın hepsine bağlı ve ikisinin çıktısını tek bir release artifact'ında birleştiriyor:

# Aynı run'ın api ve web artifact'larını manifest ile tek bir release artifact'ında birleştirir.
parameters:
  - name: manifestScript
    type: string
    default: scripts/write-release-manifest.sh

jobs:
  - job: Package
    displayName: Package release
    dependsOn:
      - Version
      - Api
      - Web
      - Audit
    condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
    variables:
      appVersion: $[ dependencies.Version.outputs['setVersion.appVersion'] ]
    steps:
      - checkout: self
        fetchDepth: 1

      - download: current
        artifact: api

      - download: current
        artifact: web

      - script: |
          mkdir -p $(Build.ArtifactStagingDirectory)/release
          cp -R $(Pipeline.Workspace)/api $(Build.ArtifactStagingDirectory)/release/api
          cp -R $(Pipeline.Workspace)/web $(Build.ArtifactStagingDirectory)/release/web
          bash ${{ parameters.manifestScript }} $(Build.ArtifactStagingDirectory)/release
        displayName: Assemble release folder
        env:
          APP_VERSION: $(appVersion)

      - task: PublishPipelineArtifact@1
        displayName: Upload release artifact
        inputs:
          targetPath: $(Build.ArtifactStagingDirectory)/release
          artifact: release

download: current, aynı run'da yüklenmiş bir artifact'ı $(Pipeline.Workspace)/<artifact-name> klasörüne indiriyor. Belgelere göre artifact'lar yalnızca deployment job'larında kendiliğinden indiriliyor; normal bir job'da bu step'i açıkça yazman gerekiyor. Koşul, pull request doğrulamasında paket üretmeyi atlıyor; ikinci yazıdaki kuralı uyguladık ve succeeded()'i koşulun içinde tuttuk. Manifest script'i paketin ne taşıdığını tek dosyada topluyor. Yerelde gerçek dotnet publish ve ng build çıktılarıyla, örnek değerler vererek çalıştırınca:

$ APP_VERSION=1.0.0-main.1287 BUILD_SOURCEVERSION=3f9c2e1a7b4d5e6f8091a2b3c4d5e6f708192a3b BUILD_BUILDNUMBER=main-20261115.3 bash scripts/write-release-manifest.sh rel
{
  "version": "1.0.0-main.1287",
  "commit": "3f9c2e1a7b4d5e6f8091a2b3c4d5e6f708192a3b",
  "run": "main-20261115.3",
  "createdUtc": "2026-11-15T09:00:34Z",
  "api": { "files": 59 },
  "web": { "files": 4 }
}

"Build once, deploy many" ilkesinin bu seri için anlamı bu: altıncı yazıdaki deploy stage'leri kodu yeniden derlemeyecek, bu release artifact'ını indirip ortamdan ortama taşıyacak. Kavramın kendisi CI/CD Temelleri serisinin "Build Once, Deploy Many: Artifact, Versiyonlama ve Immutable Paket" yazısında anlatıldı.

Run Numarası

Run listesinde her run'ın adı varsayılan olarak $(Date:yyyyMMdd).$(Rev:r) biçiminde. Uygulama pipeline'ının en üstüne bir name: satırı ekledik:

name: $(SourceBranchName)-$(Date:yyyyMMdd).$(Rev:r)

Böylece run listesinde main-20261115.3 gibi, hangi branch'ten ve hangi gün geldiği belli adlar görünüyor. $(Rev:r) yalnızca bu alanda çalışıyor ve adın geri kalanı değişince 1'den yeniden başlıyor; gün değişince ya da başka bir branch'e geçince sayaç sıfırlanıyor. Run adı Build.BuildNumber değişkeninden okunuyor ve manifest'e de bu değer yazılıyor. Uygulamanın sürümü ise ayrı: appVersion ikinci yazıdaki script'ten geliyor. İkisini birleştirmek istersen script'e ##vso[build.updatebuildnumber] logging command'ı eklenebiliyor.

Üretimde Dikkat

Cache her zaman kazandırmıyor. Cache task'ı run başında bir arşiv indirip açıyor, job sonunda yeni bir anahtar varsa klasörü paketleyip yüklüyor. Bookstore gibi iki projeli bir solution'da NuGet restore zaten birkaç saniye sürüyorsa, arşivin indirilmesi ve yüklenmesi bundan uzun sürebilir. Karar vermenin tek yolu ölçmek: cache'li ve cache'siz birkaç run'ın step sürelerini run özetinden karşılaştır. npm tarafında belgeler node_modules'u değil npm'in kendi cache klasörünü saklamayı öneriyor, çünkü npm ci her seferinde node_modules'u silip baştan kuruyor.

Paralellik, sahip olduğun parallel job sayısıyla sınırlı. Tek parallel job'lı bir organization'da Api, Web ve Audit sırayla koşar; job'ları bölmek bu durumda toplam süreyi kısaltmıyor, VM açılışları yüzünden uzatıyor. Ölçtüğün süre "kuyruk + VM açılışı + iş" toplamı; run özetindeki zaman çizelgesi hangi parçanın büyük olduğunu gösteriyor.

Pipeline cache ve pipeline artifact belgelere göre bütün katmanlarda ek ücret olmadan kullanılabiliyor, ama artifact'ları ne kadar tuttuğun run retention ayarlarına bağlı. Production'a giden bir paket, run silindiğinde kaybolmamalı; uzun süre saklanacak sürümler için retention kuralları ve paket feed'leri devreye giriyor. Feed tarafı bir sonraki yazının konusu.

Sık Düşülen Tuzaklar

Test step'ine continueOnError: true eklemek. Kırmızı bir test yüzünden pipeline'ın durmasını istemeyen biri bunu ekleyince job "SucceededWithIssues" durumunda biter. Belgelere göre job seviyesindeki succeeded() kısmen başarılı job'ları da başarılı sayıyor; yani Package job'ı çalışır ve testleri kırık kodun release paketi üretilir. Test sonucunu görmek istiyorsan doğru araç continueOnError değil, yayınlama task'larındaki succeededOrFailed().

Coverage task'ını v1'den v2'ye input'larıyla taşımak. Eski bir pipeline'daki codeCoverageTool ve reportDirectory satırları v2'de karşılıksız; task sürümünü değiştirirken input listesini de belgeden yeniden kontrol et.

Cache anahtarına işletim sistemini koymamak. Aynı pipeline'da Linux ve Windows job'ları varsa, anahtarda $(Agent.OS) olmadan bir işletim sisteminin paket klasörü ötekine geri yüklenebiliyor. Belgelerdeki örneklerin hepsi bu parçayı içeriyor.

Lock dosyasını commit'lememek. RestorePackagesWithLockFile açıkken lock dosyası repository'de değilse, CI'daki --locked-mode ilk run'da hata verir ya da her run farklı bir anahtar üretir. Lock dosyaları kodun parçası; pull request'te diff'leri de okunmalı.

Artifact'ı tek bir job'da üretip başka bir job'da dosya sistemi üzerinden aramak. Her job ayrı bir VM'de; Api job'ının $(Build.ArtifactStagingDirectory)'si Package job'ında yok. Job'lar arası dosya taşımanın yolu her zaman yükle ve indir.

Küçük Bir Deneme

★☆☆ Cache'li ve cache'siz iki run'ın "Restore (locked)" ve "npm ci" step sürelerini karşılaştır. Bookstore için cache'in ne kadar kazandırdığını not et.

★★☆ Web job'ının coverage/bookstore-web/cobertura-coverage.xml dosyasını ve Api job'ının Cobertura dosyasını ayrı artifact'lar olarak yükle; Package job'ında ikisini indirip tek bir PublishCodeCoverageResults@2 ile yayınla.

★★★ release artifact'ına bir .artifactignore ekleyerek *.pdb dosyalarını dışarıda bırak ve manifest'e her dosyanın içerik özetini (SHA-256) ekleyen bir satır yaz. Bunun deploy aşamasında hangi soruyu cevaplamayı kolaylaştıracağını düşün.

API'nin ve Angular'ın çıktısı artık tek bir paket hâlinde. Ama Bookstore'un bazı parçaları başka ekiplerin de kullanacağı kütüphaneler: sipariş olaylarının contract'ları gibi. Bir sonraki yazıda bunları Azure Artifacts feed'ine NuGet paketi olarak yayınlayacak, nuget.org ve npmjs'i upstream source olarak bağlayıp view'larla promotion yapacağız.