Advanced CI/CD for JavaScript: GitHub Actions in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Advanced CI/CD for JavaScript Projects with GitHub Actions



Automating build, testing, and deployment (CI/CD) is an integral part of modern JavaScript development. If you have already mastered basic GitHub Actions pipelines, it's time to move to the next level. In this article, we will cover advanced techniques: dependency caching, matrix builds, parallel task execution, secure secret management, and automatic deployment to VPS and Docker. All examples are focused on Node.js projects.



1. Optimizing Build Time: Caching and Conditional Triggers



One of the main problems with basic pipelines is the long installation of node_modules on every run. GitHub Actions allows you to cache dependencies, reducing execution time by 2-4 times.



Use the actions/cache action to cache node_modules or ~/.npm:



name: CI Pipelineon: [push, pull_request]

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

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20'

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

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


Advanced tips:

  • Use hashFiles('package-lock.json') for precise cache invalidation when dependencies change.
  • For monorepos, cache each package separately using path: packages/*/node_modules.
  • Add conditional triggers (if: github.ref == 'refs/heads/main') so that heavy tasks (e.g., building a production version) run only on pushes to the main branch.


2. Matrix Builds and Parallel Tasks



The matrix strategy allows you to run the same workflow with different parameters. This is indispensable for testing on multiple Node.js versions or operating systems.



jobs:  test:    strategy:      matrix:        node-version: [18, 20, 22]        os: [ubuntu-latest, windows-latest]    runs-on: ${{ matrix.os }}    steps:      - uses: actions/checkout@v4      - name: Use Node.js ${{ matrix.node-version }}        uses: actions/setup-node@v4        with:          node-version: ${{ matrix.node-version }}      - run: npm ci      - run: npm test


For parallel execution of independent tasks (e.g., linting, unit tests, integration tests), use separate jobs. They run simultaneously and do not block each other:



jobs:  lint:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - run: npm ci      - run: npm run lint

unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run test:unit

integration-tests: needs: [lint, unit-tests] # wait for lint and unit to complete runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run test:integration


Important: Use needs to define the execution order of dependent tasks. This saves time and resources.



3. Advanced Deployment: Docker and VPS Using Artifacts



After successful testing, you need to deploy the application. Let's look at two popular scenarios: deploying a Docker image and deploying to a VPS.



3.1. Building and Publishing a Docker Image



jobs:  build-and-push:    needs: [test]    runs-on: ub                

Blogs

Book Recommendations