🎓 Testing and Deployment with NX
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?
- Identify Affected Apps: Using
nx show projects --affected
, I detect which apps need new Docker images. - Authenticate: Log in to the GitHub Container Registry.
- 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
- 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.
- 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.
- 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
- For Loop:
for app in $apps
: Iterates over each app listed in the apps environment variable. - 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.
- Dynamic Behavior:
Only apps detected as “affected” by changes will have Docker images built and pushed.
Example
This workflow should:
- Detect that
app1
andapp2
are affected. - Build Docker images for these apps.
- Push images with the following tags:
- ghcr.io/your-repo/app1:v1 (your release tag on github)
- ghcr.io/your-repo/app1:latest
- ghcr.io/your-repo/app2:v1
- ghcr.io/your-repo/app2:latest
With this configuration, new Docker images will be created only if the related project has been modified. 👌