Angular CI/CD with GitHub Actions: Test, Build and Deploy Automatically

Angular Tutorial: Beginner to Advanced · 18. Quality, security and delivery · 4 min read · Updated

Manual checks get forgotten. A CI pipeline (continuous integration) runs lint, tests and the build on every change, so broken code never reaches your main branch. CD (continuous delivery or deployment) then publishes a passing build automatically. GitHub Actions is built into GitHub and free for small projects.

What you will learn

  • A workflow that lints, tests and builds on every pull request
  • How to run Playwright tests in CI
  • How to deploy with secrets and keep releases safe

Note: the commands below (ng test --watch=false, ng build, npx playwright test) were run locally while preparing this course. The workflow files are standard GitHub Actions configuration, but we could not run GitHub's servers from here, so test them in your own repository.

Scripts first

Make sure your package.json has clear scripts, because the pipeline just calls them:

{
  "scripts": {
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test --watch=false",
    "lint": "ng lint",
    "e2e": "playwright test"
  }
}

ng lint exists after you run ng add angular-eslint.

The CI workflow

Create .github/workflows/ci.yml:

name: CI

on:
  pull_request:
  push:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

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

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Unit tests
        run: npm test

      - name: Production build
        run: npm run build

      - name: Upload build
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

What it does:

  • Runs on every pull request and on pushes to main.
  • concurrency cancels older runs of the same branch, so you only wait for the latest.
  • Uses a Node version supported by your Angular version. Check the requirement of your CLI.
  • npm ci installs exactly what the lock file says, which makes builds repeatable.
  • cache: npm speeds up repeated runs.
  • The build is stored as an artifact, so the deploy job can reuse the same files that were tested.

Pin action versions (@v4) and review what third-party actions do, since they run with access to your repository.

Add end-to-end tests

  e2e:
    runs-on: ubuntu-latest
    needs: test-and-build
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test
        env:
          CI: true
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

needs makes e2e run only after the fast checks pass. On failure the report is uploaded, so you can open it from the run page.

Deploy

A simple, common deploy copies the built files to a server over SSH. Keep credentials in repository secrets (Settings, Secrets and variables, Actions), never in the file:

  deploy:
    runs-on: ubuntu-latest
    needs: [test-and-build, e2e]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Prepare SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts

      - name: Upload files
        run: rsync -az --delete dist/<project-name>/browser/ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/var/www/my-app/"

Replace <project-name> with your project's folder in dist. Other targets (Netlify, Firebase Hosting, Vercel, Azure, S3 with CloudFront) have official actions or CLIs, and many deploy straight from a Git push without a custom workflow.

Safety habits:

  • Deploy only from `main`, and protect main so changes need a pull request with passing checks.
  • Use an environment such as production with required reviewers if you want a manual approval step.
  • Give the deploy key access to one server and one folder only.
  • Keep a way to roll back: keep the previous build, or redeploy an earlier commit.
  • Never print secrets in logs.

Other useful checks

  • npm audit --audit-level=high to catch known vulnerable packages.
  • Dependabot or Renovate (a small config file) to open pull requests for updates.
  • Bundle budgets in angular.json: the production build fails when bundles grow too large.
  • Lighthouse CI to track performance and accessibility scores over time.
  • Commit message or pull request templates to keep reviews consistent.

Try it yourself

  1. Add ci.yml to a project and open a pull request. Watch the checks run.
  2. Break a test on purpose and see the pipeline fail. Fix it and push again.
  3. Add a deploy job for your host of choice, using secrets.

Quick check

  1. Why use npm ci instead of npm install in CI?
  2. Why does the deploy job only run on main?
  3. Where should deploy credentials be stored?

Next, continue with Angular Troubleshooting: 404 on Refresh, ExpressionChanged, EventTarget and SSL on Localhost.

Frequently asked questions

How long should a pipeline take?

Aim for a few minutes. Cache dependencies, run slow end-to-end tests after the fast checks, and run only what changed in big repositories.

Do I need CD for a small project?

Not at first. CI gives most of the value. Add automatic deployment when manual releases start to feel slow or risky.