You are currently viewing Better Practices in CI, .NET, and GitHub Actions: Build Stages

Better Practices in CI, .NET, and GitHub Actions: Build Stages

Part 3 of 3 of the Better CI In Stages series

Portfolio Project

This series covers an example for .NET CI. This post will cover the CI build and test stages.

CI Build Stage Yaml

  
  build:
    name: Build API
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash

    steps:       
       
    - name: Checkout
      uses: actions/checkout@v3

    - name: Cache the nuget for the app project
      id: cache-nuget
      uses: actions/cache@v3
      with:
        path: nuget
        key: nuget-${{ hashFiles('LabDemo.MinimalApi/LabDemo.MinimalApi.csproj')}}

    - name: Install dependencies
      if: steps.cache-nuget.outpus.cache-hit != true
      run: dotnet restore

    - name: Validate directory state
      run: |
        du -sh ./*

    - name: Build just the API subsystem
      run: dotnet build $PROJECT/$PROJECT.csproj -c Release --no-incremental -o $BUILD_PATH

    - name: Validate directory state
      run: |
        du -sh ./*

    - name: Upload artifact for test and publish jobs
      uses: actions/upload-artifact@v3
      with:
        name: ${{env.BUILD_ARTIFACT_NAME}}
        path: ${{env.BUILD_PATH}}
        retention-days: 1

About Caching and External Dependencies

The CI build stage handles the initial caching, restoring dependencies if needed.

Keying of Cache

The cache step on line 14 will attempt to find a cache keyed to the csproj file of the app and not the entire solution.

I’d prefer to set the name of the key for the cache as a workflow-global variable, but hashFiles as a method inside an expression assigned to a variable does not work at the time of this writing, so I’m forced to hardcode the name of the project for the key — for now,

  • We only need to carry forward the external dependencies for the app itself to the other jobs
  • We want to refresh the cache if any of the app’s dependencies change, per the app’s csproj file, so the key is based on a hash of that file

The restore of external dependencies for the app happens at this stage, but you could certainly move it to its own stage prior to the build stage, if a system grows complex enough or other factors change.

You’ll notice several examples out there where the hashFiles method uses a package-lock file. My default .NET template didn’t generate one, and I didn’t configure it to do so. I think detecting csproj changes is a more direct check.

Don’t Rely on Cache

As a better practice, build steps are best if they don’t rely on successful caching, so, here, on line 21, if we have no cache, we restore the dependencies, then line 30 will also restore as part of the build if the dependencies are still not present.

Caching is an optimization, but it isn’t guaranteed to always work. You might need to regenerate cached files in each job that needs them. — GitLab

Nuget Configuration

This example comes with an added nuget.config that specifies the name of the directory to restore external dependencies to — the directory that will be cached

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<config>
		<add key="globalPackagesFolder" value="nuget" />
	</config>
	<packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

A number of ways exist to tell dotnet about nuget dependencies. However, using a nuget config file when performing discrete dotnet steps seems the most reliable.

  • In terms of pointing to the right place for the dependencies already restored
  • As checked by manually running dotnet commands one by one and checking speed and directory sizes

About Preserving Build Output

Then, the CI build stage also ensures the build output is available for later jobs.

At line 36, the script stores the build artifact.

The build artifact is only needed for the test and publish steps in this workflow, but no way exists currently to scope it to just the workflow or to mark it as temporary to the run — for now,

  1. The script places the post-build output in a specified directory
    • Instead of using Microsoft dotnet defaults
    • Provides readability, maintainability, less cognitive load, and, indirectly, testability
  2. The script marks the specified directory as an artifact
  3. Later jobs will access this build artifact

The build artifact is not needed once the workflow completes.

How do we manage the accumulation of build artifacts over time?

Presently, our best option at this point is to mark it for retaining for 1 day only on line 41, even though once the workflow completes, the artifact is no longer needed.

About Validation in the CI Build Stage

For validating behavior, manual inspection has its limits.

In this example, per the screenshot, we can see that

  1. The cache key changed, and so the script attempted a restore
  2. The restore succeeded, as the nuget folder exists and has content
  3. The build succeeded, as the build directory is present with content

Furthermore, we can also tell how long our workflow takes from GitHub Actions.

After the CI Build Stage

Next, we’re ready to take a look at the test stage.

CR Johnson

As a software engineer with over a decade of experience working for Fortune 50 companies developing software for Windows, the web, and a few interplanetary spacecraft, she's programmed in a plethora of languages including the C#/ASP.NET stack and, recently, Rails. She has tweaked more CSS files than she can count and geeks out a little on data and SQL databases. In her spare time she works on her first novel and enjoys bicycling and dark chocolate.

This Post Has One Comment

Comments are closed.