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 ciAdvanced 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 testFor 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:integrationImportant: 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