Structure of a Build
Private build
Integration build
The private build only runs on a single developer workstation, and it is a tool to know that immediate changes did not destabilize the application. The integration build runs on a shared server and belongs to the team. It builds code from many developers. With the rise in popularity of branching models, the integration build has been adapted to run on feature branches as well as the master branch. Before we move on to how to implement our builds, let’s review the structure and flow of a build process.
Flow of a Build on a Feature Branch
When you change code, you will run your private build at every stopping point. This keeps you safe. You will learn right away if you accidentally broke something. Because you at working in Git, a decentralized version control system, you’ll make many, short commits. This enables you to undo changes very easily. Based on your judgment, you’ll run the private build locally. In our application, it is a PowerShell script and is described in more detail later in this chapter. When you decide to push changes to your team’s Git server, the CI build will detect those changes and run the integration build process on the team’s build server. Upon success, the build will archive the built artifacts, most likely in Azure Artifacts, a NuGet repository. Then an automated deployment script will trigger and deploy those built artifacts to an environment dedicated to the continuous integration process. The best name for this environment is the “TDD environment.” The purpose of this environment is to validate that (1) the new version of the software can be deployed and (2) the new version of the software still passes all its acceptance tests. This does require that you have full-system acceptance tests in your code base. If you don’t, they are easy to start developing. After the acceptance tests succeed and you determine your changes are complete, you, as the developer, will create a pull request so that your team knows that you believe the work on your branch is complete and that the code is ready to be inspected for inclusion in the master branch.
Flow of a Build on the Master Branch
Once a pull request has been approved, your branch is automatically merged into master. This is true whether you are using GitHub or Azure Repos. The CI build, which is monitoring for changes, will initiate. Upon success, the build artifacts will be stored in Azure Artifacts as NuGet packages. Then the build will be deployed to the TDD environment for validation of deployability and for the running of the automated full-system acceptance tests. Once these acceptance tests complete successfully, the build is considered a valid release candidate. That is, it is a numbered candidate for potential release and can be validated further in manual testing environments (or even additional automated testing environments) and deployed along the pipeline toward production. Figure 6-2 shows the life cycle of a master branch build.
The deployable package for a software build can be as simple as a zip file, but in .NET, the NuGet package is the standard, and these are meant to be archived in Azure Artifacts.
Steps of a Build
- 1.
Arrange: In any validation, whether an automated test, a manual test, a static analysis run, or a CI build, the validation process is responsible for setting up an environment in which it can run.
- 2.
Act: In this step, you execute a process, run some code, kick off a procedure, and so on.
- 3.
Assert: Finally, you see how things went. You check to make sure that what did happen was in line with what you expected to happen. If what happened met expectations, your validation has succeeded. If it didn’t meet expectations, your validation has failed.
Start: The private build will be triggered on demand by a developer. The CI build will be triggered by a watcher on the Git repository – when a new commit occurs.
Clean: Any temporary directories or files are deleted, and any remnants of previous builds are expunged.
Version: The build number is pushed into any areas of input needed for the resulting executable software to be stamped with the version number of the build. It’s common for a private build to have a hard-coded version such as 0.0.0 or 9.9.9 so that anyone observing can immediately tell that a build is from a private build. In Azure Pipelines, the build number will come in from an environment variable, and the build script should push this number into relevant places, such as an AssemblyInfo.cs file for .NET Framework or the dotnet.exe command line for .NET Core. If this step is omitted, resulting .NET assemblies will not be properly labeled with the build number.
Migrate Database: This step represents anything environmental that the application needs in order to function. Most applications store data, so a database needs to be created and migrated to the current schema in preparation for the subsequent build steps. In this book, we show examples using a SQL Server relational database schema.
Compile: This step transforms source files into assemblies, and performs any encoding, transpiling,3 minification, and so on to turn source code into a form suitable for execution in the intended runtime environment.
Unit Tests: This is the first step that falls into the Assert category. Now that we have a form of the software that can be validated, presuming the compile step succeeded, we start with the fastest type of validations. Unit tests execute classes and methods that do not call out of process. In .NET, this is the AppDomain, which is the boundary for a space of memory. Therefore, unit tests are blazing fast.
Integration Tests: These tests ensure that various components of the application can integrate with each other. The most common is that our data access code can integrate with the SQL Server database schema. These tests execute code that traverses across processes (.NET AppDomain, through the networking stack, to the SQL Server process) in order to validate functionality. These tests are important, but they are orders of magnitude slower than unit tests. As an application grows, expect about a 10:1 ratio of unit tests to integration tests.
Private Build Success: After these steps, a private build is done. Nothing further is necessary to run on a developer workstation.
Static Code Analysis: Whether it be the FxCop family of analyzers, products like Ndepend or SonarQube, or JavaScript linters, a CI build should include static code analysis in its list of validations. They are easy to run and find bugs that automated tests will not. Capers Jones includes them in the top 3 defect detection methods from his research.4
Publish Test Results: At this point, the CI build has succeeded and needs to output the build artifacts. Each application type has a process that outputs the artifacts in a way that is suitable for packaging, which is the next step.
Package: In .NET, this is the act of taking each deployable application component and compressing it into a named and versioned NuGet package, for example, UI (ASP.NET web site), database (SQL Server schema migration assets), BatchJob (Windows service, Azure Function, etc.), and acceptance tests (deployable tests to be run in further down the DevOps pipeline). These NuGet packages are to be pushed to Azure Artifacts. While it is possible to use zip files, NuGet is the standard package format for .NET.
Publish: Pushing the packaged NuGet files to Azure Artifacts so they are available through the NuGet feed.
CI Build Success: The continuous integration build has now completed and can report success.
Your implementation of a private build and a CI build can vary from the examples shown in this book but take care to include each of the preceding steps in a fashion that is suitable for your application. Now that you know the structure of the builds, let’s cover how to configure and run them in a .NET environment.
Using Builds with .NET Core and Azure Pipelines
Commit
Automated acceptance tests
Manual validations
Release
The commit stage includes the private build and continuous integration build. The automated acceptance test stage includes your TDD environment with the test suites that represent acceptance tests. The UAT environment, or whatever name you choose, represents the deployed environment suitable for manual validations. Then, the final release stage goes to production where your marketplace provides feedback on the value you created for it. Let’s look at the configuration of the private build and of Azure Pipelines and see how to enable the commit stage of continuous delivery.
Enabling Continuous Delivery’s Commit Stage
This is a simple private build script, but it scales with you no matter how much code you add to the solution and how many tests you add to these test suites. In fact, this build script doesn’t have to change even as you add table after table to your SQL Server database. This build script pattern has been tested thoroughly over the last 13 years across multiple teams, hundreds of clients, and a build server journey from CruiseControl.NET to Jenkins to Bamboo to TeamCity to VSTS to Azure Pipelines. Although parts and bits might change a little, use this build script to model your own. The structure is proven.
Many of the defaults are suitable for CI builds and don’t have to be customized. Let’s go through the parts that are important. First, you’ll choose your agent pool. I’ve chosen hosted agent for Visual Studio 2019. For the purposes of illustration, I’m using the build designer rather than the YAML option. All the builds and release definitions in Azure Pipelines are being converted to YAML. At the time of this writing, the YAML tooling, editor, and marketplace integration were not yet deployed. Because of this, the designer provides the full editing experience. Expect the YAML experience to enhanced quickly. When it is fully complete, you’ll be able to save your CI build configuration as a YAML file in your Git repository right next to your application. You will want to do this because any logic not versioned with your code could break your pipeline since it is inherently not compatible with branching given that only one version of the build configuration exists.
We are using NUnit as our automated testing framework for this application. Notice that we hard-code very little in formulating our commands. This is to make our build script more maintainable. It can also be standardized someone across our teams and other applications given that the variances occur in the properties at the top of the file. Pay special attention to the arguments –no-restore and –no-build. By default, any call to dotnet.exe will recompile your code and perform a NuGet restore. You do not want to do this, as it is precious time wasted and creates new assemblies just before they are tested.
After the build script finishes, we can run our static analysis tools and then push the application with its various components to Azure Artifacts as ∗.nupkg files, which are essentially ∗.zip files with some specific differences.
Notice that the build time is over 4 minutes. This is a simple application, but your build time is already up to 4 minutes and 38 seconds. Yet, your private build runs in about 1 minute locally. This is because of the hosted build agent architecture. As soon as you have your build stable, you’ll want to start tuning it. One of the first performance optimizations you can make is to attach your own build agent so that you can control the processing power as well as the levels of caching you’d like your build environment to use. Although hosted build agents will certainly improve over time, you must use private build agents in order to achieve the short cycle time necessary to move quickly. And the 3 minutes overhead you incur at the time of this writing for hosted agents is not what you want for short cycle times across your team.
At the time of this writing, internal Microsoft teams use private build agents in order to achieve the performance and control necessary for complex projects. Use the hosted agents to stabilize new build configurations. Then measure and tune them to decide if you need to provision your own private agents.
Wrap Up
./build.ps1
Bibliography
Beck, K. (2002). Test Driven Development: By Example. Addison-Wesley Professional.
Duvall, P. M. (2007). Continuous Integration: Improving Software Quality and Reducing Risk. Addison Wesley.
Humble, J. a. (2010). Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation. Addison-Wesley.
Jones, C. (2012). Retrieved from SOFTWARE DEFECT ORIGINS AND REMOVAL METHODS: www.ifpug.org/Documents/Jones-SoftwareDefectOriginsAndRemovalMethodsDraft5.pdf
Preston-Werner, T. (n.d.). Retrieved from Semantic Versioning 2.0.0: https://semver.org/
TypeScript in Visual Studio Code. (n.d.). Retrieved from https://code.visualstudio.com/docs/languages/typescript