Skip to content

double-em/ci-cd-lecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The GitHub CI/CD guide for .NET 5/6 Test

  1. Creating a Containerized .NET App
  2. The Build & Publish Pipeline
  3. Running Tests as part of the Pipeline

1. Creating a Containerized .NET App

First we need to containerize our application to make sure we have the same reproducible building steps. This prevents the classic "It builds and works on my machine". This way it doesn't matter which machine is building the image. It's always build the same way.

(Option 1): Using Visual Studio / Rider

  1. First open Visual Studio or Rider.
  2. Choose the template
    • Rider: ASP.NET Core Wep Application
    • Visual Studio: ASP.NET Core Web API. 1
  3. Give it a name.
    3.1. (Rider | Optional): Enable versioning by Selecting Create Git repository.
    3.2. (Rider): Choose the type Web API
  4. Enable Docker Support
    1. Choose Linux if using Linux containers i.e. WSL or Hyper-V (Recommended)
    2. Choose Windows if using Windows containers i.e. Hyper-V
  5. You should now have something matching a picture below:
  • Rider Pasted image 20211113114745
  • Visual Studio 3
  1. Press Create.

(Option 2): Using the dotnet CLI

Create the solution folder:

mkdir ci-cd-lecture

Change directory to the solution folder:

cd ci-cd-lecture

Create the project inside the solution folder:

dotnet new webapi -o MyWebApi

Create a Dockerfile in the project directory with the contents from [[#The Docker file]] section.

The Dockerfile

You should now have the following Dockerfile in your project directory:

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore "MyApi/MyApi.csproj"
COPY . .
WORKDIR "/src/MyApi"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

Base

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

...

This is the base of which our image is build upon.

Build

...

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore "MyApi/MyApi.csproj"
COPY . .
WORKDIR "/src/MyApi"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build

...

Here we first copy the MyWebApi.csproj project file and then restore our NuGet packages.
We then copy the entire solution to our image and then builds the Release version of our application.

Publish

...

FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish

...

Here we make dotnet publish our application which builds and optimizes our code and artifacts ready to release.

Final

...

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

Finally we copy the app from where we published our application in the publish step.
This is crucial for having a well optimized and small image to deploy later on. Because we avoid copying all the other cached files or source code that would otherwise just bloat our image for no reason.

Test the build

From the solution folder, build the image:

docker build -t myapi -f MyAPI/Dockerfile .

Run the container:

 docker run --rm -it -e ASPNETCORE_ENVIRONMENT=Development -p 80:80 myapi

Go to the swagger UI to test it out:
http://127.0.0.1/swagger/index.html

Press Ctrl+C in the terminal to stop the container.


2. The Build & Publish Pipeline

We are going to use GitHub Actions in this example for simplicity and easy access for everyone, but the general concepts apply to all CI/CD pipeline tools.

Building the Image

name: Image Build Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:

  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Build the Docker image
      run: docker build . --file MyApi/Dockerfile --tag myapi:$(date +%s)

Test it out!

Add a new commit and push it to the main branch and see if it executes.

If you go to the Actions tab in the GitHub repository, you should see something like the following: Pasted image 20211117182211

Extending the Pipeline with Publishing

name: Image Build Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name}}
  IMAGE_NAME: myapi


jobs:

  build-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Docker Setup Buildx
        uses: docker/setup-buildx-action@v1.6.0
            
      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v1.10.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Build Image
        run: |
          docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f MyApi/Dockerfile .
      - name: Push Image
        if: github.event_name != 'pull_request'
        run: |
          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

Test it out!


3. Running Tests as part of the Pipeline

All this is cool, but we need to make sure our tests pass, before we publish anything.

Create a Test project

Create a new test project in your solution and reference your API project.

In your WeatherForecastController add the following method:

...

public bool ReturnTrue()
{	
	return true;
}

...

Add the NuGet package: Moq.

Then add the following unit test:

using Microsoft.Extensions.Logging;
using Moq;
using MyApi.Controllers;
using Xunit;

namespace MyApiTest
{
    public class WeatherForecastControllerTest
    {
        [Fact]
        public void ShouldBe_ReturnTrue()
        {

            var logger = new Mock<ILogger<WeatherForecastController>>();
            var _sut = new WeatherForecastController(logger.Object);
            
            Assert.True(_sut.ReturnTrue());
        }
    }
}

Run the tests in the pipeline

Add the test project to the build step in the Dockerfile:

RUN dotnet restore "MyApiTest/MyApiTest.csproj"

So it should look like:

...

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build  
COPY ["MyApi/MyApi.csproj", "MyApi/"]  
COPY ["MyApiTest/MyApiTest.csproj", "MyApiTest/"]  
RUN dotnet restore "MyApi/MyApi.csproj"  
RUN dotnet restore "MyApiTest/MyApiTest.csproj"  
COPY . .  
RUN dotnet build "MyApi/MyApi.csproj" -c Release -o /app/build

...

Add the following new step to the Dockerfile build:

...

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS test  
COPY --from=build . .  
RUN dotnet test "MyApiTest/MyApiTest.csproj"

...

Test it out!

Try making a commit and push it to the main branch.
You should see a successful action execution like earlier.

Try to change the newly added controller method to return false instead of true:

...

public bool ReturnTrue()
{	
	return false;
}

...

Make a new commit and push it to the main branch.
You should now see it fail and if we look in the log you should see something familiar to the following image: failing-test

Done!