Github

Github provides a lot of functionality out of the box for a small fee. It is true that choosing other providers might offer additional functionality, but this mostly comes at a price. As an enterprise the decision really depends on the added value of the functionality. This mostly boils down to reporting, visualization and ease of use. In beginning projects I would advocate to use Github and the functionality it offers to the fullest.

Github Actions

Github actions allow us to automate a lot of things, that would otherwise require more expensive tools. The trade of is, that it is not a point and click interface. The advantage is, that it is widely used and therefor a lot of documentation can be found online. Github actions allow us to build and deploy our application automatically after changes are pushed to a branch.

NuGet Registry

GithubNuGet registry

To automatically create a NuGet package from code, all we need to do is create a file .github/forkflows/release.yaml from the root of our project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
name: Build Nuget Packages
on:
  workflow_dispatch: # Allow running the workflow manually from the GitHub UI
  push:
    branches:
      - 'main'

env:
  NuGetDirectory: ${{ github.workspace}}/nuget

jobs:
  create_nuget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # Get all history to allow automatic versioning using MinVer
      
      # Install the .NET SDK indicated in the global.json file
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 7.0.x #${{ matrix.dotnet-version }}

      - name: Update path
        run: export PATH="/usr/share/dotnet:$PATH"

      - name: Install dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore
      
      #      - name: Test
      #        run: dotnet test --no-restore --verbosity normal
      
      
      # Create the NuGet package in the folder from the environment variable NuGetDirectory
      - run: dotnet pack --configuration Release --output ${{ env.NuGetDirectory }}
      
      # Publish the NuGet package as an artifact, so they can be used in the following jobs
      - uses: actions/upload-artifact@v3
        with:
          name: nuget
          if-no-files-found: error
          retention-days: 7
          path: ${{ env.NuGetDirectory }}/*.nupkg

      - name: Display generated packages
        run: ls -la ${{ env.NuGetDirectory }}/*.nupkg
  

  run_test:
    runs-on: ubuntu-latest
    needs: [ create_nuget ]
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 7.0.x #${{ matrix.dotnet-version }}

      - name: Update path
        run: export PATH="/usr/share/dotnet:$PATH"
      - name: Run tests
        run: dotnet test --configuration Release

  deploy:
    # Publish only when creating a GitHub Release
    # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
    # You can update this logic if you want to manage releases differently
    #if: github.event_name == 'release'
    runs-on: ubuntu-latest
    needs: [ run_test ]
    steps:
      # Download the NuGet package created in the previous job
      - uses: actions/download-artifact@v3
        with:
          name: nuget
          path: ${{ env.NuGetDirectory }}

      # Install the .NET SDK indicated in the global.json file
      - name: Setup .NET Core
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 7.0.x #${{ matrix.dotnet-version }}

      - name: Update path
        run: export PATH="/usr/share/dotnet:$PATH"

      # Publish all NuGet packages to NuGet.org
      # Use --skip-duplicate to prevent errors if a package with the same version already exists.
      # If you retry a failed workflow, already published packages will be skipped without error.
      - name: Publish NuGet package
        shell: pwsh
        run: |
          foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) {
              dotnet nuget push $file --api-key "${{ secrets.NUGET_STORE_API_KEY }}" --source "${{ secrets.NUGET_STORE }}" --skip-duplicate
          }          
Important

Versioning is not done automatically. You need to manually set your version in the project file in your solution. The build will not fail, it will not push the new version when it already exists on the NuGet registry.

Optional: Remove the parameter --skip-duplicate from the Publish NuGet package command.

Explaining the code:

  • line 5: Script will run when changes are pushed to the main branch
  • line 9: Sets the nuget directory for the build, this will not influence your build.
  • line 23: This example depends on dotnet 7.0.x, where x is a wildcard to get the most recent version.
  • line 32: Runs the build with the release option enabled.
  • line 99: The option --skip-duplicate is used to prevent failure when the version already exists. Remove this if you want the build to fail on duplicate versions.
Note

Before uploading this, make sure to create the secrets in your github account.

  • NUGET_STORE_API_KEY: Your api key with access to the NuGet registry
  • NUGET_STORE: Url to your NuGet registry

Docker container registry

GithubDocker registry

We can create docker containers and deploy them to the Github container registry easily.

Note

You can easily deploy your containers from Github actions. Personally I prefer Gitops where we just create the container. Another service will pick it up and will deploy it.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
name: Release Workflow

on:
  push:
    branches:
      - 'release/*.*.*'
      - 'main'
      - 'develop'

env:
  ASPNETCORE_ENVIRONMENT: Development
  REGISTRY: "ghcr.io/microservice-world"
  IMAGE_NAME: "my-api"
  REGISTRY_USER: my-github-username
jobs:
  version:
    name: Determine version
    runs-on: ubuntu-latest
    outputs:
      branchName:            ${{ steps.version_step.outputs.branchName }}
      fullSemVer:            ${{ steps.version_step.outputs.fullSemVer }}
  
      GitVersion_BranchName: ${{ steps.version_step.outputs.GitVersion_BranchName }}
      GitVersion_SemVer: ${{ steps.version_step.outputs.GitVersion_SemVer }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v1.1.1
        with:
          versionSpec: '5.x'

      - name: Determine Version
        id: version_step # step id used as reference for output values
        uses: gittools/actions/gitversion/execute@v1.1.1
          
      - name: Display GitVersion variables (without prefix)
        run: |
             echo "SemVer (env.semVer)            : ${{ steps.version_step.outputs.semVer }}"             
    
    
  build:
    name: build
    needs: version
    runs-on: ubuntu-latest
    outputs:
      DockerImage: "${{ env.IMAGE_NAME}}:${{ needs.version.outputs.GitVersion_SemVer }}"
      DockerImageUrl: "https://github.com/${{ github.repository }}/pkgs/container/${{ env.IMAGE_NAME}}"
    steps:
      - name: Checkout the repo
        uses: actions/checkout@v3

      - name: Build container image
        run:  |
          docker build  \
              --no-cache \
              -f Dockerfile \
              -t $(echo $REGISTRY)/$(echo $IMAGE_NAME):${{ needs.version.outputs.GitVersion_SemVer }} \
              -t $(echo $REGISTRY)/$(echo $IMAGE_NAME):latest \
              --label "org.opencontainers.image.source=https://github.com/$(echo $REGISTRY)/$(echo $IMAGE_NAME)" \
              .          
      - name: Login GITHUB container store
        run:  docker login ghcr.io -u $(echo $REGISTRY_USER) --password ${{ secrets.GITHUB_TOKEN }}

      - name: Push image to Github Container Registry
        run:  docker push $(echo $REGISTRY)/$(echo $IMAGE_NAME):${{ needs.version.outputs.GitVersion_SemVer }}

  
  release:
    name: release
    needs: [version, build]
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
    env:
      myvar_fullSemVer: ${{ needs.version.outputs.SemVer }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 0


      - name: Setup dotnet
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: |
            8.0.x            
          
      - name: Create Change log
        id: changelog
        uses: TriPSs/conventional-changelog-action@v5.1.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          output-file: "false"
          preset: "conventionalcommits"
          skip-git-pull: true
          git-pull-method: --force
          skip-commit: true
          pre-release: true
          skip-version-file: true
          skip-tag: true
#          fallback-version: v${{ steps.version_step.outputs.semVer }} 
          fallback-version: v${{ needs.version.outputs.GitVersion_SemVer }}
          release-count: 0
          skip-on-empty: false
          tag-prefix: "v"
      #          create-summary: true
      #          skip-bump: true

      - name: Create tag
        uses: actions/github-script@v5
        continue-on-error: true
        with:
          script: |
                  github.rest.git.createRef({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    ref: 'refs/tags/${{ needs.version.outputs.GitVersion_SemVer }}',
                    sha: context.sha
                  }) || true
                                    
      - name: Create GitHub Release
        uses: ncipollo/release-action@v1
        with:
          skipIfReleaseExists: true
          name: v${{ needs.version.outputs.GitVersion_SemVer }}
          tag: v${{ needs.version.outputs.GitVersion_SemVer }}
          body: "${{ steps.changelog.outputs.clean_changelog }} \r\n \r\n ### Related docker images \r\n * [${{ needs.build.outputs.DockerImage }}](${{ needs.build.outputs.DockerImageUrl }})"
          makeLatest: true
          

Explaining the code

  • line 6 -> 7: sets up release management based on branching
  • line 10 -> 14: Environment variables needed for the build
Important

Setting the above variables is enough to build the project. This script was used for deploying a dotnet core application. It uses GitVersion to automatically determine the version numbers upon every build. The method here is only compatible with dotnet core. For more information for other software check out: https://gitversion.net/docs/

Jobs

  1. Version: determines the version number using GitVersion
  2. Build: Builds the docker container using the Dockerfile and pushes the image to the container registry
  3. Release: Creates a release using Semantic Versioning

You will be able to see the result in your repository