React App CI/CD Pipeline Using Azure DevOps

Author: Ramesh Kanjinghat

Recently, I created a simple React application. It is a single-page application (SPA) without a server-side backend.

Converting this standalone React application into an ASP.NET Core React application was also relatively straightforward. If you are interested in learning how I did that, check out my earlier post: Convert a React App to an ASP.NET Core React App.

I primarily work with Microsoft .NET technologies and use Azure DevOps for project management, Git repositories, and CI/CD pipelines. In my current organization, I also help design, implement, and maintain CI/CD pipelines across multiple projects.

Naturally, Azure DevOps was my first choice for this project as well.

Azure DevOps provides built-in Node.js tasks that can be used to build and test React applications. However, I prefer using PowerShell tasks and scripts wherever possible. I find that PowerShell gives me greater flexibility and control over the build and deployment process. This is, of course, a personal preference, and I’d be interested to hear your thoughts.

Pipeline Setup

Here are a few principles I follow when designing Azure DevOps pipelines:

  • Use YAML pipelines: Azure DevOps YAML pipelines provide a convenient way to define, version-control, and maintain CI/CD pipelines alongside application code.
  • Break the pipeline into reusable stages: I prefer dividing the pipeline into individual stages that can be reused across different pipelines, such as pull request, CI, and production pipelines.
  • Give each stage a specific responsibility: Each stage should perform a well-defined task, such as building the application, running unit tests, publishing build artifacts, or deploying the application.
  • Make stages configurable: Think of each stage as a reusable function that accepts parameters from the pipeline. For example, CI, staging, and production pipelines can pass different configuration values and deployment locations to the same stage.

The stages in my pipeline are:

  • Build: Builds the application to identify compilation errors and other build-related issues.
  • Test: Runs unit tests to identify functionality issues.
  • Publish: Builds the application using the specified configuration and publishes the resulting build artifacts to Azure Pipelines, TFS, or a file share.
  • Deploy: Downloads the published artifacts and deploys them to the target environment.

One disadvantage of separating these stages is some duplication of work. For example, the Build, Test, and Publish stages each retrieve the source code, install application dependencies, and perform a build. However, I find that the ability to reuse these stages across multiple pipelines outweighs the overhead of this duplication.

Note: Don’t confuse the Publish stage in this pipeline with the dotnet publish command.

The dotnet publish command prepares an application for deployment by generating the deployment-ready output. In contrast, the Publish stage in this pipeline uploads the generated build artifacts to a temporary storage location. These artifacts can then be downloaded and deployed to multiple environments without rebuilding the application.

Learn more:

Pipelines

I have three separate pipelines to handle different stages of the development and release process.

1. Pull Request Pipeline

This pipeline is triggered whenever a pull request is created against a configured branch, typically the CI or master branch.

Its primary purpose is to validate the incoming changes before they are merged. It checks for build errors, runs unit tests, and can also perform code coverage analysis.

Therefore, this pipeline only needs the Build and Test stages.

2. CI Pipeline

This pipeline is triggered whenever new code is merged into the CI branch.

It builds and tests the application, publishes the build artifacts, and includes the deployment stage to deploy the application to the CI environment.

3. Master Pipeline

This pipeline is triggered whenever new code is merged into the master branch.

Like the CI pipeline, it builds and tests the application and publishes the build artifacts. It also includes the deployment stage, which can deploy the application to one or more environments depending on your release requirements.

Before looking at the individual stages and pipelines, let’s examine the package.json file and see how I have configured the application for different environments.

Environment Configuration in package.json

The package.json file contains custom scripts that help build, test, and deploy the application to different environments.

Let’s look at the scripts section:

"scripts": {
  "install-dependencies": "npm install --save-dev",
  "start": "npm-run-all -p watch-css start-js",
  "build:ci": "env-cmd -f .env.ci react-scripts build",
  "build:staging": "env-cmd -f .env.staging react-scripts build",
  "build:production": "env-cmd -f .env.production react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "start-js": "react-scripts start"
}

The scripts used by the pipeline stages are:

  • install-dependencies: Installs the application dependencies.
  • build:ci: Builds the application using CI environment configurations.
  • build:staging: Builds the application using staging environment configurations.
  • build:production: Builds the application using production environment configurations.
  • test: Runs the application’s unit tests.

I use the open-source npm package env-cmd to load environment-specific configuration variables.

The following dependency is included in my package.json:

"env-cmd": "10.1.0"

I use separate environment files (.env.ci, .env.staging, and .env.production) to maintain configuration values for each environment.

You can learn more about environment-specific configuration in the Create React App documentation.

Pipeline Stages

Now, let’s look at how each stage is configured in Azure DevOps.

Build

Build stage in Azure CI/CD pipeline

The Build stage retrieves the source code, installs the required dependencies, and builds the React application.

Test

Test stage in Azure CI/CD pipeline

The Test stage executes the application’s unit tests to verify that the changes haven’t introduced any functionality issues.

Publish

Publish stage in Azure CI/CD pipeline

The Publish stage builds the application using the specified environment configuration and uploads the resulting artifacts to the configured artifact storage location.

Deploy

Deploy stage in Azure CI/CD pipeline

The Deploy stage downloads the published artifacts and deploys them to the target environment.

Azure DevOps environments can be used to organize deployments and manage environment-specific approvals and deployment history.

Learn more about Azure DevOps environments.

Azure DevOps Pipelines

Now that we have defined the individual stages, let’s look at how they fit together in the three pipelines.

Pull Request Pipeline

Azure CI/CD pull request pipeline

The Pull Request pipeline runs the Build and Test stages to validate incoming changes before they are merged.

Historical note: At the time of writing, Azure DevOps did not support defining YAML pull request triggers for repositories hosted in Azure Repos Git. Instead, pull request validation had to be configured using branch policies.

CI Pipeline

Azure CI/CD CI pipeline

The CI pipeline runs the Build, Test, Publish, and Deploy stages. It uses the CI-specific configuration to build and deploy the application to the CI environment.

Master Pipeline

Azure CI/CD Master/Production pipeline

The Master pipeline follows the same process as the CI pipeline but uses production-specific configuration and deployment settings.

If you compare the CI and Master pipelines, you will notice that the main differences are the following parameter values:

Parameter CI Pipeline Master Pipeline
buildConfiguration ci production
environmentName CI Production

The same reusable stages are used in both pipelines. Only the parameters passed to those stages change.

If you want to deploy the application to a staging environment before production, you can introduce another deployment stage and pass the appropriate configuration values, such as:

  • buildConfiguration: staging
  • environmentName: Staging

You can adapt this approach to support as many environments as your deployment process requires.

Conclusion

Azure DevOps YAML pipelines make it relatively straightforward to build a reusable CI/CD process for a React application.

By separating the process into Build, Test, Publish, and Deploy stages, we can reuse the same pipeline logic across pull requests, CI, staging, and production deployments.

This approach also makes the pipeline easier to maintain because environment-specific differences are handled through parameters rather than duplicating the entire pipeline configuration.