Update on 2nd January 2020
The bug described in the post which prevented the Bundle folder to be included in the final AppX / MISX package has been fixed. As such, the step in the pipeline to workaround the bug has been removed, since it isn't needed anymore.
After working for a while on React Native for Windows, I've started pretty soon to research how to build a CI/CD pipeline for such a project. I'm a big fan of Azure DevOps and having an automated cycle where I can continuously deliver new versions of my application just by committing new code to my repository is priceless for me. Now it's a good time to start playing with CI/CD for React Native for Windows: as I have mentioned at the end of my previous post about building native modules, now you can build self-contained bundles for Windows, which don't require the Metro packager to be up & running. It's enough to start the traditional process in Visual Studio to create an app package and choose the Release configuration mode to get an AppX / MSIX package that can be published on the Microsoft Store, side-loaded, deployed in an enterprise using SSCM or Intune, etc.
However, when you start exploring this option, you face a few challenges, since the hosted agents provided by Azure DevOps miss some of the requirements to compile the various projects included in the React Native for Windows implementation. Let's see how to properly setup our pipeline.
The first step, as always, is to have the source code of your React Native project committed on a source control repository. In my case I hosted it on GitHub, but you can choose Azure Repos or any other Git provider. Then you need to move to the Pipelines section of your Azure DevOps project (create one if you don't have it) and click on Create pipeline.
As first step, choose the repository where your code is hosted. As second step, you will be asked for a starting template for the YAML file, which defines the tasks that our pipeline will have to follow. Thanks to YAML you can enable what is called the Infrastructure As Code approach, which allows to provision hardware resources using a configuration file rather than interactive tools like a wizard. The advantage of this approach is that the configuration of your infrastructure (in this case, a build machine) becomes part of your project: it can be committed to the repository, it can be versioned, it can be easily replicated.
The best template for our scenario is the Universal Windows Platform one, since React Native for Windows generates, under the hood, a UWP application.
This is the default YAML that gets created:
# Universal Windows Platform # Build a Universal Windows Platform project using Visual Studio. # Add steps that test and distribute an app, save build artifacts, and more: # https://aka.ms/yaml trigger: - master pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'x86|x64|ARM' buildConfiguration: 'Release' appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: platform: 'x86' solution: '$(solution)' configuration: '$(buildConfiguration)' msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)" /p:AppxPackageDir="$(appxPackageDir)" /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload'
Now we need to make a few changes. Let's see them, step by step.
By default, the Universal Windows Platform template leverages the latest Windows hosted agent, which ships with Visual Studio 2019. However, there's a catch in our scenario. Many of the C++ projects involved in the React Native implementation are leveraging the C++ v141 toolset platform, which belongs to Visual Studio 2017. The Visual Studio installer offers you the option to install the v141 toolset in Visual Studio 2019 but, unfortunately, the windows-latest hosted agent doesn't include it. As such, we need to fallback to the hosted agent with Visual Studio 2017, by setting the
vmImage property of the YAML file to
vs2017-win2016, as in the following example:
pool: vmImage: 'vs2017-win2016'
There are a few changes to make here compared to the default configuration. The most important one is the
solution parameter, which by default is set to compile every solution included in the repository. However, React Native for Windows doesn't ship as a library or a NuGet package, but the whole source code is included as a Node module. As such, we need to specify that we want to compile only the solution which is included in the windows folder, which is the one that will generate the final AppX / MSIX package. As per the build platform, you're free to choose the ones you prefer: since React Native for Windows generates a Universal Windows Platform application, it supports x86, x64 and ARM. The build configuration, instead, must be Release: as we have learned in the previous post, this is the compilation mode which generates a self-contained application.
This is how the
variables section of our YAML file should look like:
variables: solution: 'windows/*.sln' buildPlatform: 'x64' buildConfiguration: 'Release' appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\'
This isn't specific for React Native, but it applies to every application packaged as MSIX. If you want to have a reliable CI / CD pipeline, you must increase the version number at every build. Regardless if you choose to publish the app on the Microsoft Store or to deploy it in another way, an update must always have a higher version number than the previous release.
The easiest way to achieve this task is to leverage the build number, since Azure DevOps automatically generates a new one at every build. However, the default one isn't a good fit for a packaged application. By default, in fact, Azure DevOps uses the following rule:
which is translated to a build number like:
If you have some experience with packaged applications, you'll immediately realize that this number won't work. The manifest of an AppX / MSIX package, in fact, requires the version number to follow the rule x.y.z.0, so something like:
As such, we need to change the default build number, by adding the following entry in the YAML file, before the
The next step is to inject this build number in the manifest of our application. The easiest way to do it is to install, in our Azure DevOps account, a 3rd party extension called Manifest Versioning Build Task, developed by Richard Fennell. Once we have added it, we can simply add, as fist task in the
steps section, the following entry:
- task: VersionAPPX@2 displayName: 'Version MSIX' inputs: Path: '$(Build.SourcesDirectory)' VersionNumber: '$(Build.BuildNumber)' InjectVersion: true
This task will simply take the build number (stored in the
$(Build.BuildNumber) variable) and set it inside the manifest of our application.
The hosted agent comes with Node.js already installed. However, React Native requires a specific version to work properly. Using a newer version can lead to errors during the bundling process. As such, we need to add a task to install and set the correct Node.js version as default on the hosted agent, which is 12.9.1:
- task: UseNode@1 inputs: version: '12.9.1'
React Native for Windows projects are compiled using, as target SDK, the 1903 (build 18836) one. However, since this version is fairly new, it's included only in the windows-latest hosted agent and not in the vs2017-win2016 one we're using. As such, if we want to avoid compilation errors, we need to install the SDK first on the hosted agent. The easiest way to do it is to leverage Chocolatey, the popular package manager for Windows. Think of it like NuGet, but for Windows applications instead of development libraries. Among the many packages it offers, we can find also the Windows 10 1903 SDK. Chocolatey is already installed on every Windows hosted agent, so you just need to add the following task:
- script: choco install windows-sdk-10-version-1903-all
By default, the dependencies of a React Native application are not committed inside the repository, but they are restored the first time you build the application. This is a standard approach. This isn't code we have written, so it wouldn't make sense to increase the size of our repository for files that can be easily downloaded from Internet. We see the same, for example, with NuGet packages leveraged by .NET applications. As such, before building our project, we need to make sure that all the modules are downloaded and installed on the hosted agent. To achieve this goal we can use Yarn, which is installed as well on every hosted agent. As such, we just need to add the following task:
- script: yarn install
At some point, during the creation of the MSIX package, Visual Studio will launch the
react-native command is part of the React Native CLI, which isn't installed by default on the hosted agent. We need to install it first as a global tool, using NPM. However, there's a catch. When you install a package as global with NPM, it's installed only for the current user. The hosted agent, instead, performs all the tasks using a service which runs using a dedicated account, called VssAdministrator. As such, we need to install the React Native CLI for this user, otherwise the Visual Studio build will fail because it won't find it.
NPM supports setting the folder where to install global packages, by using the
config parameter. Here are the tasks we need to launch:
- script: npm config set prefix C:\Users\VssAdministrator\AppData\Roaming\npm - script: npm install -g react-native-cli
The first script sets, as installation folder for NPM packages, the AppData folder which belongs to the VssAdministrator user. The second script, instead, installs the React Native CLI in that folder.
Before moving on we need to restore the NuGet packages. Many of the React Native for Windows projects, in fact, have dependencies on NuGet packages which are required by the whole solution to build.
- task: NuGetCommand@2 inputs: command: 'restore' restoreSolution: 'windows/*.sln' feedsToUse: 'select'
As you can notice, also in this case we specify that we want to restore only the NuGet packages referenced by the main solution, which is stored inside the windows folder.
Now we can finally build the Visual Studio solution. Here we don't have to make too many changes to the default settings. This is the task I'm using:
- task: VSBuild@1 inputs: solution: '$(solution)' msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)" /p:AppxPackageDir="$(appxPackageDir)" /p:AppxBundle=Never /p:UapAppxPackageBuildMode=SideloadOnly /p:AppxPackageSigningEnabled=false' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'
In my case, I opted to generate a MSIX package and not a bundle (
/p:AppxBundle=Never) and to use it for sideloading (
/p:UapAppxPackageBuildMode=SideloadOnly). If you want to generate a bundle (which is suggested if you want to support multiple CPU architectures or you have lot of graphical assets), just set the
/p:AppBundle parameter to
Always. And if you want to publish your application on the Microsoft Store, just leave the
/p:UapAppxPackageBuildMode parameter set to
StoreUpload. The only parameter which I strongly encourage to add is
/p:AppxPackageSigningEnabled=false, which will disable package signing during the build process. If you want to publish your application on the Microsoft Store, you don't need to digitally sign the package since the Store will do it for you. If, instead, you want to release it in other ways, you have to sign it but doing it during the build process performed by Visual Studio isn't considered a best practice. You would need, in fact, to upload the certificate on your repository, leaving you vulnerable to identity theft.
Congratulations! Now, every time you'll commit new code to the repository, a build will be triggered and the hosted agent will create a new MSIX package with the latest version. That's the Continuous Integration part of our DevOps story. What about the Continuous Deployment? Once we have a MSIX package, we need to deploy it so that our users can get the most up-to-date version. We have different options: we can publish the package on the Microsoft Store; or we can leverage a technology called App Installer to deploy the package on a website and enable automatic updates from there.
To achieve this goal you're going to build a release pipeline, which will allow you to deploy your application in multiple stages. Azure DevOps offer many built-in tasks which makes the deployment easier: you have tasks to connect to the Microsoft Store; tasks to digitally sign the package with a certificate; tasks to copy the package on an Azure Storage; etc.
I won't explain all the details in this article. You can find all the information about the various options you have and how to implement them in the last chapter of my recently released e-book, called MSIX Succinctly, which is available for free thanks to Syncfusion. Alternatively, this topic is covered also by Exercise 6 of the Windows applications modernization workshop that my team has built.
In this post we have learned how to create a CI/CD pipeline for a React Native for Windows project. But there's a catch. Let's say you have a project which includes some custom native modules you have built, following the approach described in my previous post.
If you setup a pipeline with the guidance described in this article, you'll start to hit a series of errors like the following ones during the Visual Studio build task:
[error]node_modules\react-native-windows\Microsoft.ReactNative.SharedManaged\AttributedViewManager.cs(447,21): Error CS8107: Feature 'default literal' is not available in C# 7.0. Please use language version 7.1 or greater. [error]node_modules\react-native-windows\Microsoft.ReactNative.SharedManaged\JSValue.cs(105,36): Error CS8107: Feature 'readonly references' is not available in C# 7.0. Please use language version 7.2 or greater.
The Microsoft.ReactNative.SharedManaged project, which contains the implementation of the various classes and attributes which make easier to turn a class into a native module for React Native, leverages many features of the C# language that has been added after the 7.0 release. Many of these features require Visual Studio 2019, while our hosted agent is running Visual Studio 2017.
Unfortunately, as we have learned in the beginning, due to the requirement of supporting the v141 C++ platform toolset we can't just switch our pipeline to use the windows-latest hosted agent. As such, we need to build a self-hosted agent with all our requirements.
But that's food for the next blog post =) In the meantime, you can review the final version of the pipeline we have built in a sample React Native project I put together on GitHub. The repository contains a file called azure-pipelines.yml, which contains the full YAML file.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.