🎓 Testing and Deployment with NX

🎓 Testing and Deployment with NX
Photo by Ian Taylor / Unsplash

Why Use NX for Affected Projects?

When working in a monorepo or managing multiple apps, running all tests or building every project for every change becomes time-consuming and resource-intensive. NX simplifies this by identifying affected projects based on changes in your codebase. This ensures that only the necessary steps are executed for relevant projects.

Testing Only Affected Projects

Here’s an example of my GitHub Actions configuration for running tests efficiently:

name: Testing

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  actions: read
  contents: read

jobs:
  install-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v4
        name: Install pnpm

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

      - name: Install dependencies
        run: pnpm install

      - name: Fetch main branch if needed
        if: github.ref != 'refs/heads/main'
        run: git rev-parse --verify main || git remote set-branches origin main && git fetch --depth 1 origin main && git branch main origin/main

      - uses: nrwl/nx-set-shas@v4

      - run: pnpm exec nx affected:lint
      - run: pnpm exec nx affected:test --ci

Key Steps Explained:

  • Install Dependencies: Ensures the project is ready for testing.
  • Determine Affected Projects: The nrwl/nx-set-shas action compares the current branch with the main branch to detect affected projects.
  • Run Tests: The nx affected:test command executes tests only for modified projects.

Deploying Only Affected Projects

Efficient deployments are just as critical. I use NX to build and push Docker images only for projects affected by code changes:

- name: Derive SHAs for `nx affected` commands
  id: setSHAs
  uses: nrwl/nx-set-shas@v1

- run: |
    echo "BASE: ${{ steps.setSHAs.outputs.base }}"
    echo "HEAD: ${{ steps.setSHAs.outputs.head }}"

- name: Detect affected apps
  id: affected
  run: |
    AFFECTED_APPS=$(pnpm exec nx show projects --affected | tr '\n' ' ')
    echo "Affected apps: $AFFECTED_APPS"
    echo "apps=$AFFECTED_APPS" >> $GITHUB_ENV

- name: Log in to GitHub Container Registry
  run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Build and push Docker image
  run: |
    for app in $apps; do
      echo "Building and pushing Docker image for $app..."
      pnpm exec nx run $app:docker-build --tag=ghcr.io/${{ github.repository }}-$app:${{ github.ref_name }} --tag=ghcr.io/${{ github.repository }}-$app:latest
      docker push ghcr.io/${{ github.repository }}-$app:${{ github.ref_name }}
      docker push ghcr.io/${{ github.repository }}-$app:latest
    done

What’s Happening Here?

  1. Identify Affected Apps: Using nx show projects --affected, I detect which apps need new Docker images.
  2. Authenticate: Log in to the GitHub Container Registry.
  3. Build and Push: For each affected app, a Docker image is built and pushed with relevant tags.

Detailed explanations

Step 1: Detect Affected Apps

- name: Detect affected apps
  id: affected
  run: |
    AFFECTED_APPS=$(pnpm exec nx show projects --affected | tr '\n' ' ')
    echo "Affected apps: $AFFECTED_APPS"
    echo "apps=$AFFECTED_APPS" >> $GITHUB_ENV
  1. Command Breakdown:

pnpm exec nx show projects --affected this NX command identifies all projects affected by changes between the current branch and the base branch (usually main).

tr '\n' ' ' converts the output list of affected projects into a space-separated string for easier looping in later steps.

  1. Environment Variable:

echo "apps=$AFFECTED_APPS" >> $GITHUB_ENV command stores the list of affected apps in the GitHub Actions environment as a variable named apps.

  1. Output Example:

Suppose you modify files related to two projects, app1 and app2. The output would be:
Affected apps: app1 app2

Step 2: Log in to GitHub Container Registry

- name: Log in to GitHub Container Registry
  run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

Nothing fancy here, just a common way to connect

Step 3: Build and Push Docker Images

- name: Build and push Docker image
  run: |
    for app in $apps; do
      echo "Building and pushing Docker image for $app..."
      pnpm exec nx run $app:docker-build --tag=ghcr.io/${{ github.repository }}-$app:${{ github.ref_name }} --tag=ghcr.io/${{ github.repository }}-$app:latest
      docker push ghcr.io/${{ github.repository }}-$app:${{ github.ref_name }}
      docker push ghcr.io/${{ github.repository }}-$app:latest
    done
  1. For Loop: for app in $apps: Iterates over each app listed in the apps environment variable.
  2. Docker Build and Push:
  • Build: pnpm exec nx run $app:docker-build
    Runs the docker-build target for the app using NX. This assumes your workspace.json or project.json contains a target named docker-build for each project.
  • Tagging: --tag=ghcr.io/${{ github.repository }}-$app:${{ github.ref_name }}
    Tags the image with a unique identifier for the branch (e.g., main or a specific feature branch).
    Also tags it with latest for easy retrieval of the most recent image.
  • Push: docker push
    Pushes the tagged images to the GitHub Container Registry.
  1. Dynamic Behavior:

Only apps detected as “affected” by changes will have Docker images built and pushed.

Example

This workflow should:

  1. Detect that app1 and app2 are affected.
  2. Build Docker images for these apps.
  3. Push images with the following tags:
    1. ghcr.io/your-repo/app1:v1 (your release tag on github)
    2. ghcr.io/your-repo/app1:latest
    3. ghcr.io/your-repo/app2:v1
    4. ghcr.io/your-repo/app2:latest

With this configuration, new Docker images will be created only if the related project has been modified. 👌