Skip to main content

1 - GitHub Actions: CI/CD for Angular Apps from Scratch

Complete guide to configuring GitHub Actions for an Angular project: build, test, lint, automatic deploy and workflow optimization.

What do you think about this article?

At a Glance

Complete guide to configuring GitHub Actions for an Angular project: build, test, lint, automatic deploy and workflow optimization.

Introduction: CI/CD for Frontend Developers

Every frontend developer eventually faces the same question: how can I automate the build, test, and deployment process for my application? The modern answer is CI/CD (Continuous Integration / Continuous Deployment), and GitHub Actions is today the most accessible and powerful tool for implementing it directly within your project repository.

In this first article of the DevOps for Frontend Developers series, we will build a complete pipeline from scratch for an Angular application: from the first continuous integration workflow to automatic production deployment. Every concept is explained with practical examples and ready-to-use code.

What You Will Learn in This Article

  • The fundamental concepts of GitHub Actions: workflows, jobs, steps, and triggers
  • How to set up a CI workflow for Angular (install, lint, test, build)
  • How to implement automatic deployment to Firebase and Vercel
  • Caching strategies to speed up pipelines
  • Matrix strategy for multi-version Node.js testing
  • How to configure artifacts and branch protection rules
  • How to create reusable workflows

1. GitHub Actions Fundamentals

GitHub Actions is an automation platform integrated into GitHub. Every automation is based on four key concepts that form a precise hierarchy:

GitHub Actions Hierarchy

Concept Description Analogy
Workflow A YAML file in .github/workflows/ that defines the entire automation The complete recipe
Job A group of steps executed on a specific runner A phase of the recipe
Step A single command or action within a job A specific instruction
Trigger The event that starts the workflow (push, PR, schedule, etc.) The moment to begin

A workflow can contain multiple jobs, and each job can contain multiple steps. Jobs run in parallel by default, but can be configured with dependencies for sequential execution.

YAML Workflow Structure

Every workflow resides in the .github/workflows/ directory of your repository. Here is the basic structure of a workflow file:


# .github/workflows/ci.yml
name: CI Pipeline

# Trigger: when to run the workflow
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

# Global environment variables
env:
  NODE_VERSION: '20'

# Jobs to execute
jobs:
  build:
    # Runner: virtual machine to run on
    runs-on: ubuntu-latest

    # Job steps
    steps:
      # Step 1: Checkout code
      - name: Checkout repository
        uses: actions/checkout@v4

      # Step 2: Setup Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      # Step 3: Install dependencies
      - name: Install dependencies
        run: npm ci

      # Step 4: Build
      - name: Build application
        run: npm run build
      

Main Triggers

Triggers determine when a workflow is started. GitHub Actions supports dozens of events, but the most commonly used for frontend projects are:


on:
  # On every push to specified branches
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'package.json'

  # On every Pull Request to main
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Scheduled execution (cron)
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6:00 UTC

  # Manual trigger
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      

Watch Out for Push Triggers

The paths filter is very useful to avoid unnecessary builds: if you only modify the README, there is no reason to re-run the entire pipeline. However, be careful not to exclude critical files like angular.json or tsconfig.json that affect the build.

2. Complete CI Workflow for Angular

Let us now build a complete Continuous Integration workflow for an Angular project. This workflow will execute sequentially: dependency installation, linting, unit tests, and production build.


# .github/workflows/ci.yml
name: Angular CI

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

env:
  NODE_VERSION: '20'

jobs:
  lint:
    name: Lint Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng lint

  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

  build:
    name: Production Build
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng build --configuration=production
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 3
      

This workflow defines three sequential jobs through the needs keyword: first lint, then tests, finally the build. If lint fails, tests are not executed; if tests fail, the build does not start.

Advanced Dependency Caching

The cache: 'npm' keyword in the setup-node action provides basic caching. For more granular control, we can use the actions/cache action directly:


  - name: Cache node_modules
    id: cache-npm
    uses: actions/cache@v4
    with:
      path: |
        node_modules
        ~/.npm
      key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-npm-

  - name: Install dependencies
    if: steps.cache-npm.outputs.cache-hit != 'true'
    run: npm ci
      

The cache key uses the hash of the package-lock.json file: when dependencies do not change, the cache is reused and installation is completely skipped. This can reduce pipeline time by 30-60 seconds per execution.

3. Matrix Strategy: Multi-Version Testing

The matrix strategy allows you to run the same job with different combinations of parameters. For an Angular project, it is particularly useful for testing across multiple Node.js versions:


  test-matrix:
    name: Test on Node ${{ matrix.node-version }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng test --watch=false --browsers=ChromeHeadless
      

The fail-fast: false option ensures that all matrix jobs complete even if one fails, allowing you to identify all incompatibilities in a single run.

When to Use Matrix Strategy

  • Testing across multiple Node.js versions (current and previous LTS)
  • Testing across multiple operating systems (ubuntu, windows, macos)
  • Building for different environments (staging, production)
  • Testing with different browser versions

4. CD: Automatic Deployment to Firebase

After building and testing the application, the next step is automatic deployment. Let us see how to configure deployment to Firebase Hosting, one of the most common choices for Angular applications.


# .github/workflows/deploy.yml
name: Deploy to Firebase

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    name: Build and Deploy to Production
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build for production
        run: npm run build

      - name: Deploy to Firebase Hosting
        uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          channelId: live
          projectId: my-angular-project
      

Preview Deploy for Pull Requests

A powerful feature of Firebase Hosting is the ability to create automatic preview channels for every Pull Request, allowing the team to verify changes before merging:


# .github/workflows/preview.yml
name: Deploy Preview

on:
  pull_request:
    branches: [main]

jobs:
  preview:
    name: Deploy Preview Channel
    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

      - name: Deploy to Preview Channel
        uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          projectId: my-angular-project
          # Without channelId: automatically creates a preview channel
      

Deploying to Vercel

For those who prefer Vercel as a hosting platform, the configuration is equally straightforward:


# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel

on:
  push:
    branches: [main]

env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

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

      - name: Install Vercel CLI
        run: npm install -g vercel

      - name: Pull Vercel Configuration
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build with Vercel
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
      

5. Artifacts: Sharing Files Between Jobs

Artifacts allow you to share files between different jobs in the same workflow. This is essential for separating the build from deployment without recompiling:


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npx ng build --configuration=production

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: angular-dist
          path: dist/my-app/browser/
          retention-days: 1

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: angular-dist
          path: dist/

      - name: Deploy
        run: |
          # Build content is now in dist/
          ls -la dist/
      

Artifact Best Practices

  • Always set retention-days to avoid wasting storage space
  • Use descriptive names for artifacts (angular-dist, coverage-report)
  • Upload only the necessary files, not the entire node_modules directory
  • For very large files, consider compression before uploading

6. Branch Protection Rules

GitHub branch protection rules allow you to enforce constraints on critical branches. When combined with CI workflows, they create an automatic quality gate.

To configure branch protection rules, go to Settings > Branches > Add branch protection rule in your GitHub repository. The recommended settings for the main branch are:

  • Require a pull request before merging: Prevents direct pushes to main
  • Require status checks to pass before merging: CI workflows must pass
  • Require branches to be up to date before merging: Branch must be current
  • Require conversation resolution before merging: All comments must be resolved

To select which status checks to require, you must first run the workflow at least once. After that, job names will appear in the selection list:


Required status checks:
  [x] Lint Check          (from CI workflow)
  [x] Unit Tests          (from CI workflow)
  [x] Production Build    (from CI workflow)
      

7. Reusable Workflows

When managing multiple repositories with similar configurations, reusable workflows avoid code duplication. A reusable workflow is defined with the workflow_call trigger:


# .github/workflows/reusable-angular-ci.yml
name: Reusable Angular CI

on:
  workflow_call:
    inputs:
      node-version:
        description: 'Node.js version'
        required: false
        default: '20'
        type: string
      build-command:
        description: 'Build command to execute'
        required: false
        default: 'npm run build'
        type: string
    secrets:
      FIREBASE_SA:
        description: 'Firebase Service Account'
        required: false

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng lint
      - run: npx ng test --watch=false --browsers=ChromeHeadless
      - run: ${{ inputs.build-command }}
      

To use this reusable workflow from another workflow:


# .github/workflows/ci.yml (in the consuming repository)
name: CI

on:
  push:
    branches: [main]

jobs:
  angular-ci:
    uses: my-org/shared-workflows/.github/workflows/reusable-angular-ci.yml@main
    with:
      node-version: '20'
      build-command: 'npm run build -- --configuration=production'
    secrets:
      FIREBASE_SA: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
      

Benefits of Reusable Workflows

  • DRY: A single maintenance point for CI/CD logic
  • Standardization: All repositories follow the same practices
  • Versioning: You can reference a specific workflow version (@main, @v1)
  • Security: Secrets are passed explicitly, not shared globally

8. Complete Example: End-to-End Pipeline

Let us combine all concepts into a complete pipeline that handles CI and CD with branch protection:


# .github/workflows/pipeline.yml
name: Full Pipeline

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

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

env:
  NODE_VERSION: '20'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng lint

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npx ng build --configuration=production
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy-preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          projectId: my-angular-project

  deploy-production:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    needs: build
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          channelId: live
          projectId: my-angular-project
      

The concurrency option at the top of the workflow is crucial: if you make rapid consecutive pushes to the same branch, the previous workflow is automatically cancelled, saving execution minutes.

9. Debugging and Best Practices

When a workflow fails, debugging can be frustrating. Here are the most effective strategies:

Enabling Debug Logging

You can enable debug logs by adding secrets to the repository:

  • ACTIONS_RUNNER_DEBUG = true for detailed runner logs
  • ACTIONS_STEP_DEBUG = true for detailed step logs

Conditional Debug Steps


  - name: Debug info
    if: failure()
    run: |
      echo "Node version: $(node -v)"
      echo "NPM version: $(npm -v)"
      echo "Directory: $(pwd)"
      ls -la
      cat package.json
      

GitHub Actions Best Practices

  • Use npm ci instead of npm install for deterministic installations
  • Always pin action versions (@v4 instead of @main)
  • Configure concurrency to avoid duplicate builds
  • Use caching to reduce execution times
  • Separate jobs for fast feedback (lint fails immediately, not after the build)
  • Use environment to protect production secrets
  • Set timeout-minutes to prevent stuck jobs
  • Monitor GitHub Actions minutes consumption on your account plan

Conclusion

In this article we built a complete CI/CD pipeline for an Angular application using GitHub Actions. We explored the fundamental concepts (workflows, jobs, steps, triggers), implemented dependency caching, configured matrix strategy for multi-version testing, and set up automatic deployment to Firebase and Vercel.

The key to success with CI/CD pipelines is progressive iteration: start with a simple workflow (lint + build), then add tests, and finally automatic deployment. Do not try to implement everything at once.

In the next article of this series, we will explore Docker for frontend developers, learning to containerize an Angular application with multi-stage Dockerfiles and manage development environments with Docker Compose.

Frequently asked questions

What is the main purpose of a workflow in GitHub Actions?

A workflow in GitHub Actions is an automation platform that contains multiple jobs, and each job can contain multiple steps. Jobs run in parallel by default, but can be configured with dependencies for sequential execution.

How does the filter feature help in avoiding unnecessary builds in a pipeline?

The filter feature helps in avoiding unnecessary builds by allowing you to exclude certain files or changes from triggering the entire pipeline, such as modifying only the README file.

What is the purpose of using the cache action with a specific key in a workflow?

Using the cache action with a specific key allows for granular control over caching dependencies and can reduce pipeline time by reusing cached results when dependencies do not change.

How does the matrix strategy allow for multi-version testing in an Angular project?

The matrix strategy allows you to run the same job with different combinations of parameters, such as testing across multiple Node.js versions, ensuring that all matrix jobs complete even if one fails.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects