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.
- 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 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
- The script marks the specified directory as an artifact
- 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
- The cache key changed, and so the script attempted a restore
- The restore succeeded, as the nuget folder exists and has content
- 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.
Pingback: Better CI Test and Publish Stages for .NET - Code Onward