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-daysto avoid wasting storage space - Use descriptive names for artifacts (
angular-dist,coverage-report) - Upload only the necessary files, not the entire
node_modulesdirectory - 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=truefor detailed runner logsACTIONS_STEP_DEBUG=truefor 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 ciinstead ofnpm installfor deterministic installations - Always pin action versions (
@v4instead of@main) - Configure
concurrencyto avoid duplicate builds - Use caching to reduce execution times
- Separate jobs for fast feedback (lint fails immediately, not after the build)
- Use
environmentto protect production secrets - Set
timeout-minutesto 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.
Related deep dive: 13 - EU Normative e Data Protection - GDPR in Practice for Developers.
Related deep dive: 14 - Case Study - Implementazione End-to-End di DevSecOps Pipeline.
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.
Commenti
Caricamento commenti...
Sign in to comment