This series covers an example for .NET CI. This post will cover the CI test and publish stages.
Pipeline Parallels
The script has the test and publish jobs dependent on the build job. This allows the test and publish to run in parallel. The deploy job then requires both test and publish jobs to run.
We get the speed of running test and publish jobs concurrently, yet we still block the deploy if either fails.
CI Test Stage
The example’s test stage run tests that are essentially black-box tests of the API, written with XUnit, versus unit tests testing classes or class methods. Systems testing is considered out of scope for this example.
Test Stage Yaml
test:
name: API Black-Box Testing
runs-on: ubuntu-latest
defaults:
run:
shell: bash
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Validate directory state
run: |
du -sh ./*
- name: Nuget from cache
id: cache-nuget
uses: actions/cache@v3
with:
path: nuget
key: nuget-${{ hashFiles('LabDemo.MinimalApi/LabDemo.MinimalApi.csproj')}}
- name: Validate directory state
run: |
du -sh ./*
- name: Download artifact from build job
uses: actions/download-artifact@v3
- name: Validate directory state
run: |
du -sh ./*
# artifacts are overkill for this small of project, but done as a
# poc and best practice for larger systems
- name: Test the api
run: |
pwd
du -sh .
dotnet test $TEST_PROJECT/$TEST_PROJECT.csproj -c Release -o $BUILD_PATH
- name: Validate directory state
run: |
du -sh ./*
Caching and Dependencies
On line 23, did you notice that the cache is only the app’s external dependencies in the cache? Not the test project dependencies?
We cache app dependencies for use in the build, test, and publish jobs. The test dependencies are only needed in the test job.
The script on line 43 pulls the test dependencies from the nuget source specified in the nuget.config.
Test Output
The -o
parameter in the dotnet test
command is misleading. That’s where the build output lives — a combination of the build stage output from the cache and the build of the test project.
This is NOT where any test result lives, per the documentation. dotnet test
does have a --results-directory
if we wanted to preserve and artifact test results.
So, in this example, we rely on the the workflow job passing or failing. The job will fail if any of the tests ran by dotnet test
fail.
CI Test Validation
Through the validation output we can see
- The external dependencies pop in to the directory via the cache
- The build artifact from the app build
- Both the build and nuget subdirectories growing in size after
dotnet test
built the test project
Test Stage Summary
So, the test stage uses the app dependencies from the cache and the app build output as an artifact to save time, to save from repeating steps done in the build stage.
But, we’re not done yet — the publish stage is next. It will need the cache and artifact as well.
CI Publish Stage
This stage requires the build artifact (generated in the build stage and used by the test stage), then generates the final product to be delivered and deployed.
Publish Stage Yaml
publish:
name: Publish the API
runs-on: ubuntu-latest
defaults:
run:
shell: bash
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Validate directory state
run: |
du -sh ./*
- name: App's nuget from cache
id: cache-nuget
uses: actions/cache@v3
with:
path: nuget
key: nuget-app-${{ hashFiles('LabDemo.MinimalApi/LabDemo.MinimalApi.csproj')}}
- name: Validate directory state
run: |
du -sh ./*
- name: Download artifact from build job
uses: actions/download-artifact@v3
- name: Validate directory state
run: |
du -sh ./*
# artifacts are overkill for this small of project, but done as a
# poc and best practice for larger systems
- name: Publish the api
run: dotnet publish $PROJECT/$PROJECT.csproj -c Release -p:OutDir=$BUILD_PATH -o $PUBLISH_PATH
- name: Validate directory state
run: |
du -sh ./*
- name: Upload artifact for deployment
uses: actions/upload-artifact@v3
with:
name: ${{env.PUBLISH_ARTIFACT_NAME}}
path: ${{env.PUBLISH_PATH}}
retention-days: 1
Caching and Dependencies
On line 18 of the publish job the cache is accessed, and on line 29 the download of build artifact happens.
The dotnet publish
on line 40 will regenerate the nuget dependencies if the cache step fails to provide a cache. The artifact used in this job is the artifact generated by the build job.
Line 8 ensures the publish stage will not happen if the test stage fails.
Pointing to the Build Directory
The dotnet publish
command on line 40 includes a -o
directory where we’re directing the final product to deploy.
Less obviously, the -p:OutDir=$BUILD_PATH
points to the build artifact.
CI Publish Validation
We can validate the behavior of the publish job with manual visual inspection, checking to be sure dotnet
behaved in the desired way.
In the screenshot, we can see the nuget directory exists after the cache step. This directory remains unchanged, as expected.
We can also see the build artifact download and remain unchanged in size.
Finally, we can see the publish directory and the size of its contents.
CI Summary
In this series, we’ve seen an efficient but basic way to structure CI steps for a .NET app.
We’ve also seen a way to track that each dotnet
step is using the inputs as given, versus regenerating products already created in previous CI steps.
As a bonus, we saw how a bash script can be used locally to imitate each of the dotnet steps in these jobs, so that we can explore the behavior covered in the app’s GitHub Actions yaml.