{"id":20886133,"url":"https://github.com/double-em/ci-cd-lecture","last_synced_at":"2026-04-11T12:45:32.660Z","repository":{"id":68013112,"uuid":"426917213","full_name":"double-em/ci-cd-lecture","owner":"double-em","description":"A lecture I held for my class.","archived":false,"fork":false,"pushed_at":"2023-12-15T14:34:36.000Z","size":45,"stargazers_count":0,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-19T11:14:19.432Z","etag":null,"topics":["ci-cd","docker","dotnet-core","github-actions","lecture"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/double-em.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-11-11T08:00:48.000Z","updated_at":"2021-11-26T09:02:53.000Z","dependencies_parsed_at":null,"dependency_job_id":"3a7dcedd-235c-4070-a509-51e56feafddc","html_url":"https://github.com/double-em/ci-cd-lecture","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/double-em%2Fci-cd-lecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/double-em%2Fci-cd-lecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/double-em%2Fci-cd-lecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/double-em%2Fci-cd-lecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/double-em","download_url":"https://codeload.github.com/double-em/ci-cd-lecture/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243268085,"owners_count":20263803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ci-cd","docker","dotnet-core","github-actions","lecture"],"created_at":"2024-11-18T08:15:52.653Z","updated_at":"2026-04-11T12:45:25.409Z","avatar_url":"https://github.com/double-em.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# The GitHub CI/CD guide for .NET 5/6 Test\n1. [Creating a Containerized .NET App](#1-creating-a-containerized-NET-App)\n1. [The Build \u0026 Publish Pipeline](#2-the-build--publish-pipeline)\n1. [Running Tests as part of the Pipeline](#3-running-tests-as-part-of-the-pipeline)\n\n---\n## 1. Creating a Containerized .NET App\nFirst 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.\n\n### (Option 1): Using Visual Studio / Rider\n1. First open Visual Studio or Rider.\n2. Choose the template \n\t- Rider: `ASP.NET Core Wep Application`\n\t- Visual Studio: `ASP.NET Core Web API`.\n\t![1](https://user-images.githubusercontent.com/8335996/142277829-1a0f91d7-f9de-4275-aa32-6f6e375aabd6.png)\n3. Give it a name.  \n3.1. (Rider | Optional): Enable versioning by Selecting `Create Git repository`.  \n3.2. (Rider): Choose the type `Web API`\n7. Enable `Docker Support`\n\t1. Choose Linux if using Linux containers i.e. WSL or Hyper-V (Recommended)\n\t2. Choose Windows if using Windows containers i.e. Hyper-V\n8. You should now have something matching a picture below:\n- Rider ![Pasted image 20211113114745](https://user-images.githubusercontent.com/8335996/142277915-6d21a37f-8fe6-412f-8450-c669ce0c6797.png)\n- Visual Studio ![3](https://user-images.githubusercontent.com/8335996/142277947-e54b9c8f-3770-422a-b8e1-d4dc1d7b8be2.png)\n8. Press `Create`.\n\n\n### (Option 2): Using the dotnet CLI\nCreate the solution folder:\n```shell:\nmkdir ci-cd-lecture\n```\nChange directory to the solution folder:\n```shell:\ncd ci-cd-lecture\n```\n\nCreate the project inside the solution folder:\n```shell\ndotnet new webapi -o MyWebApi\n```\n\nCreate a `Dockerfile` in the project directory with the contents from [[#The Docker file]] section.\n\n### The Dockerfile\nYou should now have the following Dockerfile in your project directory:\n```dockerfile\nFROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:5.0 AS build\nWORKDIR /src\nCOPY [\"MyApi/MyApi.csproj\", \"MyApi/\"]\nRUN dotnet restore \"MyApi/MyApi.csproj\"\nCOPY . .\nWORKDIR \"/src/MyApi\"\nRUN dotnet build \"MyApi.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"MyApi.csproj\" -c Release -o /app/publish\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"MyApi.dll\"]\n```\n\n#### Base\n```dockerfile\nFROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\n...\n```\n\nThis is the base of which our image is build upon.\n\n#### Build\n```dockerfile\n...\n\nFROM mcr.microsoft.com/dotnet/sdk:5.0 AS build\nWORKDIR /src\nCOPY [\"MyApi/MyApi.csproj\", \"MyApi/\"]\nRUN dotnet restore \"MyApi/MyApi.csproj\"\nCOPY . .\nWORKDIR \"/src/MyApi\"\nRUN dotnet build \"MyApi.csproj\" -c Release -o /app/build\n\n...\n```\n\nHere we first copy the `MyWebApi.csproj` project file and then restore our NuGet packages.  \nWe then copy the entire solution to our image and then builds the Release version of our application.\n\n#### Publish\n```dockerfile\n...\n\nFROM build AS publish\nRUN dotnet publish \"MyApi.csproj\" -c Release -o /app/publish\n\n...\n```\n\nHere we make dotnet publish our application which builds and optimizes our code and artifacts ready to release.\n\n#### Final\n```dockerfile\n...\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"MyApi.dll\"]\n```\n\nFinally we copy the app from where we published our application in the `publish` step.  \nThis 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.\n\n\n#### Test the build\nFrom the solution folder, build the image:\n```shell\ndocker build -t myapi -f MyAPI/Dockerfile .\n```\n\nRun the container:\n```shell\n docker run --rm -it -e ASPNETCORE_ENVIRONMENT=Development -p 80:80 myapi\n```\n\nGo to the swagger UI to test it out:  \n[http://127.0.0.1/swagger/index.html](http://127.0.0.1/swagger/index.html)\n\nPress `Ctrl+C` in the terminal to stop the container.\n\n---\n## 2. The Build \u0026 Publish Pipeline\nWe 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.\n\n### Building the Image\n```yaml\nname: Image Build Pipeline\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Build the Docker image\n      run: docker build . --file MyApi/Dockerfile --tag myapi:$(date +%s)\n```\n\n#### Test it out!\nAdd a new commit and push it to the main branch and see if it executes.\n\nIf you go to the `Actions` tab in the GitHub repository, you should see something like the following:\n![Pasted image 20211117182211](https://user-images.githubusercontent.com/8335996/142278013-b70f3a6d-43ed-4b74-95fe-0df9cba3755f.png)\n\n### Extending the Pipeline with Publishing\n```yaml\nname: Image Build Pipeline\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\nenv:\n  REGISTRY: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name}}\n  IMAGE_NAME: myapi\n\n\njobs:\n\n  build-publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    \n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      - name: Docker Setup Buildx\n        uses: docker/setup-buildx-action@v1.6.0\n            \n      - name: Log into registry ${{ env.REGISTRY }}\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v1.10.0\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n          \n      - name: Build Image\n        run: |\n          docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f MyApi/Dockerfile .\n      - name: Push Image\n        if: github.event_name != 'pull_request'\n        run: |\n          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest\n```\n\n#### Test it out!\n\n---\n## 3. Running Tests as part of the Pipeline\nAll this is cool, but we need to make sure our tests pass, before we publish anything.\n\n### Create a Test project\nCreate a new test project in your solution and reference your API project.\n\nIn your `WeatherForecastController` add the following method:\n```csharp\n...\n\npublic bool ReturnTrue()\n{\t\n\treturn true;\n}\n\n...\n```\n\nAdd the NuGet package: `Moq`.\n\nThen add the following unit test:\n```csharp\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing MyApi.Controllers;\nusing Xunit;\n\nnamespace MyApiTest\n{\n    public class WeatherForecastControllerTest\n    {\n        [Fact]\n        public void ShouldBe_ReturnTrue()\n        {\n\n            var logger = new Mock\u003cILogger\u003cWeatherForecastController\u003e\u003e();\n            var _sut = new WeatherForecastController(logger.Object);\n            \n            Assert.True(_sut.ReturnTrue());\n        }\n    }\n}\n```\n\n### Run the tests in the pipeline\nAdd the test project to the build step in the Dockerfile:\n```dockerfile\nRUN dotnet restore \"MyApiTest/MyApiTest.csproj\"\n```\n\nSo it should look like:\n```dockerfile\n...\n\nFROM mcr.microsoft.com/dotnet/sdk:5.0 AS build  \nCOPY [\"MyApi/MyApi.csproj\", \"MyApi/\"]  \nCOPY [\"MyApiTest/MyApiTest.csproj\", \"MyApiTest/\"]  \nRUN dotnet restore \"MyApi/MyApi.csproj\"  \nRUN dotnet restore \"MyApiTest/MyApiTest.csproj\"  \nCOPY . .  \nRUN dotnet build \"MyApi/MyApi.csproj\" -c Release -o /app/build\n\n...\n```\n\nAdd the following new step to the Dockerfile build:\n```dockerfile\n...\n\nFROM mcr.microsoft.com/dotnet/sdk:5.0 AS test  \nCOPY --from=build . .  \nRUN dotnet test \"MyApiTest/MyApiTest.csproj\"\n\n...\n```\n\n#### Test it out!\n\nTry making a commit and push it to the main branch.  \nYou should see a successful action execution like earlier.\n\nTry to change the newly added controller method to return false instead of true:\n```csharp\n...\n\npublic bool ReturnTrue()\n{\t\n\treturn false;\n}\n\n...\n```\n\nMake a new commit and push it to the main branch.  \nYou should now see it fail and if we look in the log you should see something familiar to the following image:\n![failing-test](https://user-images.githubusercontent.com/8335996/142278041-4af6ca3d-4390-4dc8-ba19-5bfd2e7b5b09.png)\n\n## Done!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouble-em%2Fci-cd-lecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdouble-em%2Fci-cd-lecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouble-em%2Fci-cd-lecture/lists"}