Angular  

Intelligent Build System: Detect Only Impacted Angular/.NET Modules and Build

Introduction

Large enterprise monorepos quickly suffer from slow CI/CD pipelines. A full build of every Angular app, library and .NET service on every commit wastes CPU, increases feedback time, and costs money. An Intelligent Build System detects which modules are impacted by a change and builds only those — plus their dependents — while ensuring correctness.

This article explains how to design, implement and operate an intelligent build system for hybrid stacks: Angular (frontend) and .NET (backend). You will get:

  • High-level architecture and workflows (block diagrams).

  • File-to-module dependency analysis techniques.

  • Practical detection scripts and CI pipeline examples.

  • Strategies for correctness (tests, contracts, change validation).

  • Caching, distributed execution, and observability.

  • Operational guidance and common pitfalls.

The goal: faster developer feedback, lower CI cost, and reliable builds for production deployment.

Problem Statement

In a monorepo with many frontend libs, multiple Angular apps, and several .NET microservices:

  • Small UI change can re-trigger tens of builds.

  • Backend interface change requires rebuilding many services.

  • CI queues balloon and developer feedback becomes slow.

We want a system that:

  1. Determines the minimal set of modules needing rebuild for a given commit (impacted set).

  2. Builds and tests only that set (and safe dependents).

  3. Preserves correctness (no missing rebuilds).

  4. Integrates with existing CI/CD and developer workflows.

High-Level Architecture

┌─────────────┐      ┌──────────────┐      ┌───────────────┐
│  Developer  │ ---> │  CI Trigger  │ ---> │ Impact Analyzer│
└─────────────┘      └──────┬───────┘      └──────┬────────┘
                            │                    │
                            ▼                    │
                       ┌───────────┐              │
                       │ Git Diff  │<-------------┘
                       └────┬──────┘
                            │
                            ▼
                  ┌───────────────────────┐
                  │ Module Graph Service  │
                  │ (Angular TS + .NET)   │
                  └─────────┬─────────────┘
                            │
                            ▼
                 ┌──────────────────────────┐
                 │ Build Orchestrator (CI)  │
                 │ (local / remote cache /  │
                 │ distributed executors)   │
                 └─────────┬────────────────┘
                            │
                            ▼
                   ┌─────────────────┐
                   │ Artifact Store  │
                   └─────────────────┘

Key Concepts

  • Module: logical unit — Angular app/lib, .NET project, shared library.

  • Dependency Graph: directed graph where an edge A→B means A depends on B (if B changes, A may need rebuild).

  • Impacted Set: the minimal set of modules that must be rebuilt because they were changed or they transitively depend on changed modules.

  • Safe Dependents: set of modules that may be rebuilt to be conservative (e.g., if interfaces changed).

  • Change Contract: explicit markers that indicate whether a change is backwards-compatible or breaking (e.g., API version bump, public API changes).

  • Build Cache: store of previous build outputs keyed by input hash to skip rebuilds when possible.

How Impact Detection Works

1. File Diff → Affected Files

Get changed files for the commit/PR:

# changed files vs main
git fetch origin main
git diff --name-only origin/main...HEAD

2. File → Module Mapping

Map each changed file to a module. Techniques:

  • Path-based mapping: repo layout uses folders per project: libs/ui/button/**libs/ui/button module.

  • tsconfig / project.json: read angular.json or tsconfig.json projects section to know Angular projects and their root.

  • .csproj mapping: each .NET project lives in a folder; map changed .cs/.csproj file to that project.

  • Custom manifest: maintain module-manifest.json mapping globs → module name.

Example module-manifest.json:

{
  "modules": [
    { "name": "ui-button", "globs": ["libs/ui/button/**"] },
    { "name": "orders-api", "globs": ["src/Services/Orders/**"] }
  ]
}

Apply with minimatch or gitignore-style matching.

3. Module Graph Construction

Two build-time graphs required:

  • Angular graph: use ng/nx or the tsconfig project references. Tools:

    • nx graph --file=graph.json (Nx)

    • ng v8+ with ng lava? (nx recommended)

    • For manual approach: parse tsconfig references and package.json paths.

  • .NET graph: use MSBuild to list project references:

dotnet msbuild -nologo -t:GenerateRestoreGraphFile -p:RestoreGraphOutputPath=graph.dg
# or use dotnet list <proj> reference -- include-transitive
dotnet list src/Services/Orders/Orders.csproj reference

Store combined graph in a canonical format (JSON adjacency list). This graph is cached and updated when projects change.

4. Transitive Closure → Impacted Modules

For each module representing a changed file, compute all modules that depend on it (reverse graph traversal). That union is the impacted set.

If module A depends on B and B changed → A is impacted.

Optionally include only direct dependents or include transitive layers; generally transitive closure is safe.

Dealing With APIs And Contracts

Not all changes require rebuilding dependents:

  • Internal implementation change only: downstream contracts unchanged.

  • Public API change: downstream must rebuild & test.

Approaches

  1. Conservative (safe): always rebuild dependents. Simple but might rebuild too much.

  2. Contract Aware (preferred)

    • Use public API extractor (TypeScript api-extractor for libs; .NET PublicApiCompat or ApiPort) to compute API surface hash.

    • If API hash unchanged, skip dependents. If changed, include dependents.

    • For .NET, use dotnet format? Better: use PublicApiAnalyzers or extract public API via reflection/unit tests and compare snapshots.

Example flow:

  • On every merge to main, extract API snapshot per project and store in artifacts.

  • On PR, run API extractor for changed module; if snapshot differs → mark breaking change.

This enables precise decisions: implementation-only change → no dependent rebuild.

Angular Specifics

  • Angular CLI projects are declared in angular.json. Use it to map projects to folders.

  • Use ng build --project <name> and ng test --project <name> to build/test single project.

  • For TypeScript libs, tsc --build with project references provides incremental builds.

Nx (highly recommended)

  • nx affected:apps --base=origin/main gives affected apps.

  • nx affected --target=build will run build only for affected projects and their dependents.

  • Nx includes computation of dependency graph (works great for mixed TS monorepos).

If not using Nx, implement script:

changed=$(git diff --name-only origin/main...HEAD)
modules=$(node scripts/map-files-to-modules.js $changed)
impacted=$(node scripts/compute-impacted.js $modules graph.json)
for m in $impacted; do ng build --project $m; done

.NET Specifics

  • Use SDK-style projects with <ProjectReference> elements for intra-repo references; use dotnet list package to find dependencies.

  • dotnet build is incremental; MSBuild will skip projects where input hash unchanged — but only when run for the entire solution. We need to tell CI which projects to build.

Techniques

  • Use dotnet build <path-to-csproj> for each impacted project and its dependents.

  • Generate project dependency graph with MSBuild or use dotnet list <proj> reference.

  • Use msbuild /t:Restore;Build /p:BuildProjectReferences=false and then build only required projects — this avoids building the whole solution.

For interface-aware detection

  • Use a tool to compute public API (e.g., ApiCompat, PublicApiGenerator), snapshot it, and compare diffs.

CI Pipeline Examples

GitHub Actions (simplified)

name: CI
on: [push, pull_request]
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with: dotnet-version: '8.0.x'
      - name: Detect changed files
        id: diff
        run: |
          git fetch origin main
          git diff --name-only origin/main...HEAD > changed.txt
          echo "::set-output name=files::$(cat changed.txt | jq -R -s -c 'split("\n")')"
      - name: Map to modules + compute impacted
        id: impacted
        run: |
          node scripts/compute-impacted.js changed.txt graph.json > impacted.json
          echo "::set-output name=modules::$(cat impacted.json)"
  build:
    needs: analyze
    runs-on: ubuntu-latest
    strategy:
      matrix:
        module: ${{ fromJson(needs.analyze.outputs.modules) }}
    steps:
      - uses: actions/checkout@v3
      - name: Build module
        run: |
          if [[ "${{ matrix.module.type }}" == "angular" ]]; then
             ng build --project ${{ matrix.module.name }}
          else
             dotnet build ${{ matrix.module.path }} -c Release
          fi

This pattern parallelizes per module; combine with caching for speed.

Azure DevOps / GitLab

Same idea: run analyzer job and then a dynamic matrix job that builds impacted modules.

Build Cache And Distributed Execution

Building only impacted modules still benefits greatly from build cache & remote execution.

Options

  • Remote Cache: Nx Cloud, Gradle Build Cache, Bazel remote cache. For .NET, use caching via nuget and artifact caching; OSS projects exist for sourcelink-style caching.

  • Distributed Build Execution: Use Azure Pipelines distributed agents or GitHub Actions with concurrency. For larger orgs, consider Bazel or Buildbarn for hermetic distributed builds.

  • Artifact Store: push module artifacts (npm tarballs, NuGet packages, docker images) to a cache to short-circuit downstream jobs.

Cache key design: include commit hashes of module sources + relevant dependency API hashes + environment variables.

Tests: Unit, Integration, E2E

Correctness requires tests. Build system should:

  • Run unit tests for changed modules.

  • Run integration tests for dependents if API changed.

  • Run critical E2E tests selectively (smoke tests) or route full E2E to nightly pipelines.

Example rule

  • If only UI CSS changed → run unit tests and UI lint only.

  • If public API changed → run full integration test matrix for dependent services.

Use test selection:

  • For Angular: ng test --project <name> --watch=false

  • For .NET: dotnet test <proj> (use --filter to run subset).

Observability And Metrics

Track:

  • Average CI duration per PR (before & after).

  • Number of modules built per PR.

  • Cache hit rate.

  • False negatives (missed impacted modules that caused runtime failures).

  • API snapshot differences and frequency.

Store metrics in Prometheus/Grafana or cloud CI analytics. Alert on false negatives and increased E2E failures correlated with skipped builds.

Security And Supply Chain Considerations

  • Ensure analyzer scripts run in CI with minimal privileges and with controlled toolchain versions.

  • Verify generated artifacts (npm/nuget) are signed or have checksum verification.

  • Prevent arbitrary code execution: analyzer should only run allowed scripts; validate module-manifest mappings.

  • For build caches and remote executors, secure the artifact store and use signed tokens.

Operational Playbook

  • Start conservative: begin by rebuilding changed modules + direct dependents for a period (e.g., 2 weeks) while logging potential misses.

  • Instrument and move to API-aware mode (public API snapshots) when confident.

  • Maintain a “safety” full-run nightly to catch anything missed.

  • Keep manual override: allow PR to trigger full build if investigator suspects risk.

  • Keep main protected: require successful CI (full or partial as policy dictates) before merge.

Common Pitfalls And How To Avoid Them

  • Incorrect file→module mapping: maintain manifest and validate with tests.

  • Missed transitive dependencies: always compute transitive closure in graph.

  • API changes not detected: use API extractor and snapshot comparisons.

  • Build cache inconsistency: include all inputs (env vars, toolchain versions) in cache key.

  • Too-conservative rules (no gain): iterate to reduce rebuild footprint gradually.

Sample Scripts and Tools

  • scripts/compute-impacted.js — takes changed files and graph.json, outputs impacted modules.

  • scripts/extract-angular-graph.ts — parse angular.json and tsconfig references.

  • scripts/extract-dotnet-graph.ps1 — use dotnet list for project refs and produce JSON.

Small Bash example to list changed Angular projects (without Nx):

changed_files=$(git diff --name-only origin/main...HEAD)
projects=()
for projDir in $(jq -r '.projects | keys[]' angular.json); do
  root=$(jq -r ".projects[\"$projDir\"].root" angular.json)
  for f in $changed_files; do
    if [[ $f == $root* ]]; then
      projects+=($projDir)
      break
    fi
  done
done
echo "${projects[@]}"

Roadmap For Maturity

  1. Start: path-based mapping + transitive closure + conservative rebuild.

  2. Add: public API snapshot detection for safer skipping.

  3. Add: build cache + artifact store, parallelized matrix builds.

  4. Add: distributed remote execution (Bazel, BuildGrid) for large orgs.

  5. Add: automatic rollback policies and full-run verification triggers if anomalies detected.

Conclusion

An Intelligent Build System saves time and money while improving developer productivity. The core ingredients are:

  • Accurate file→module mapping.

  • Reliable dependency graph for Angular and .NET projects.

  • API-aware decisions to skip unnecessary dependent builds.

  • Robust caching, parallel execution and artifact management.

  • Sound testing and observability to guarantee safety.