Skip to content
Angular ng deployment 4 min read

CI/CD Pipelines

A CI/CD pipeline turns the manual ritual of “lint, test, build, deploy” into an automated workflow that runs on every push and pull request. For an Angular app this means catching broken code before it merges, producing a reproducible production bundle, and shipping it to your host without a human running commands locally. GitHub Actions is the most common choice because it lives next to your code, but the same stages apply to GitLab CI, CircleCI, or Azure Pipelines.

The stages of an Angular pipeline

A healthy pipeline runs a sequence of gates, each fast enough to give quick feedback and strict enough to block bad code. The typical order is: install dependencies, lint, run unit tests, build for production, then deploy. The first three stages run on every pull request; deployment usually runs only on the default branch after a merge.

StageCommandPurpose
Installnpm ciDeterministic dependency install
Lintng lintEnforce code style and rules
Testng test --watch=false --browsers=ChromeHeadlessRun unit specs headless
Buildng build --configuration productionProduce the optimized bundle
Deployhost-specificPublish the dist output

Use npm ci rather than npm install in CI. It installs exactly what is in package-lock.json, fails if the lock file is out of sync, and is significantly faster on a clean machine.

A complete GitHub Actions workflow

Create .github/workflows/ci.yml. Workflows live in .github/workflows/ and are written in YAML. This one runs the quality gates on every pull request and on pushes to main.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Unit tests
        run: npm test -- --watch=false --browsers=ChromeHeadless

      - name: Build
        run: npm run build -- --configuration production

The cache: npm option on setup-node caches the npm download directory keyed on your lock file, so subsequent runs skip re-downloading unchanged packages. ubuntu-latest ships with a headless Chrome that Karma can drive, so no extra browser install is needed.

Output:

✔ Install dependencies     12s
✔ Lint                      8s
✔ Unit tests               24s
  Chrome Headless: Executed 47 of 47 SUCCESS
✔ Build                    19s
  Output location: dist/my-app/browser

Wiring up the scripts

The workflow above calls npm run lint, npm test, and npm run build. Make sure these exist in package.json so the pipeline and local development share the same commands.

{
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint"
  }
}

If you have not added linting yet, run ng add @angular-eslint/schematics once locally; it installs ESLint and wires the lint target into angular.json.

Running tests headlessly

By default ng test opens a browser and watches files — neither works in CI. Pass flags to run once in headless Chrome, or define a CI-specific configuration in angular.json under the test builder so the pipeline command stays short.

// karma.conf.js (excerpt) — a CI-friendly headless launcher
module.exports = function (config) {
  config.set({
    browsers: ['ChromeHeadlessCI'],
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox', '--disable-gpu'],
      },
    },
  });
};

The --no-sandbox flag is required because CI runners execute as root, where Chrome’s sandbox refuses to start.

Adding a deploy job

Deployment should depend on the build job passing and run only on main. Use needs to chain jobs and an if condition to gate the branch. This example deploys static output to Netlify, but the pattern — gate, build, upload — is identical for any host.

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build -- --configuration production
      - name: Deploy to Netlify
        run: npx netlify-cli deploy --prod --dir=dist/my-app/browser
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Secrets like the auth token are stored in the repository’s Settings → Secrets and variables → Actions and injected as environment variables — never commit them to the repo.

The deploy job rebuilds rather than reusing the build job’s output, because each job runs on a fresh machine. For larger apps, upload the dist folder with actions/upload-artifact in build and download it in deploy to avoid building twice.

Caching to speed things up

Beyond the npm cache, the Angular CLI keeps its own build cache in .angular/cache. Persisting it across runs makes incremental builds faster.

      - name: Cache Angular build
        uses: actions/cache@v4
        with:
          path: .angular/cache
          key: angular-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
          restore-keys: angular-${{ hashFiles('package-lock.json') }}-

The restore-keys fallback lets a new commit reuse the previous commit’s cache even when the SHA differs, which is what makes the cache actually pay off.

Best Practices

  • Run lint, test, and build on every pull request so problems are caught before merge, not after.
  • Use npm ci for deterministic, lock-file-driven installs in every CI job.
  • Run unit tests in headless Chrome with --no-sandbox so they pass on root-based runners.
  • Gate the deploy job behind needs: and an if: github.ref == 'refs/heads/main' check so only reviewed, merged code ships.
  • Store all credentials as encrypted repository secrets and reference them through env, never in committed files.
  • Cache both the npm directory and .angular/cache to keep pipeline runs fast as the app grows.
  • Pin action versions (e.g. actions/checkout@v4) so a third-party update cannot silently change your pipeline.
Last updated June 14, 2026
Was this helpful?