Github CI/CD Auto Publishing .net Project Next Repo

Objectives:

  • we have source code in one srcRepo
  • we have destination repo [destRepo] already deployment pipeline and other things defined in another repo
  • we need to generate dll files and push that generated dll files to destRepo
  • We want to use self hosted github runner, in windows
  • self hosted runner should be running as local system account
  • We will be deploying over PreRelease Branch
  • We will not copy private, uploads appSettings.json file to published repo

What we need to do:

  • Generate Github Personal Access Token [PAT] to manage repo
  • Configure Github Repo with secrects
  • configure windows vps server with repo clone
  • Configure github repo with workflow

Generate Github Personal Access Token [PAT]

  • Login to Github
  • Navigate to Settings -> Developer settings -> fine Grain Access Token
  • Generate New -> expiry date -> choose matching org and repo
  • Add permission for Content with read and write permission

Configure Windows VPS server with Repo Clone

  • Login to windows vps server
  • open git bash/terminal and clone destRepo, provide PAT from step 1
  • add directory to git safe listing using command: git config –global –add safe.directory C:/{your path}
  • check and give ownership of the folder to same user which github self hosted runner is running

Configure github repo with secrets

  • Login to github
  • navigate to repo
  • open gettings -> action secrets
  • Add new secrets -> DEPLOY_PAT with value from step 1
  • Add new secrets -> DEPLOY_REPO_PATH with folder location from step 2

configure github workflow

  • add github repo /.github/workflows/build-artifacts.yml
name: Build .NET and sync to deployment repo (self-hosted)

on:
  push:
    branches: ["dev"]
  workflow_dispatch:

concurrency:
  group: build-sync-deploy-repo
  cancel-in-progress: false

jobs:
  build:
    runs-on: [self-hosted, windows, {runner-name}]

    env:
      CONFIGURATION: Release

      # Local path on VPS where deployment repo is already cloned
      DEPLOY_REPO_PATH: ${{ secrets.DEPLOY_REPO_PATH }}

      # Branch your deployment pipeline listens to
      DEPLOY_BRANCH: PreRelease

      # Keep last 5 rollback snapshots as branches: PreRelease-bk-1..5
      SNAPSHOT_KEEP: "5"

      # speed
      DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
      DOTNET_CLI_TELEMETRY_OPTOUT: "1"
      NUGET_XMLDOC_MODE: skip
      
      GIT_TERMINAL_PROMPT: "0"
      GCM_INTERACTIVE: "Never"

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

      - name: Show .NET SDKs (using preinstalled)
        shell: powershell
        run: |
          dotnet --info
          dotnet --list-sdks

      # ✅ Set fast persistent NuGet cache
      - name: Set NuGet cache location
        shell: powershell
        run: |
          echo "NUGET_PACKAGES=C:\nuget-cache" >> $env:GITHUB_ENV

      - name: Restore
        shell: powershell
        run: dotnet restore {.sln file path from repo root} # eg. .\srv\myapp.sln

      - name: Build (no restore)
        shell: powershell
        run: dotnet build {.sln file path from repo root} -c $env:CONFIGURATION --no-restore

      - name: Publish (no build)
        shell: powershell
        run: |
          $ErrorActionPreference = "Stop"

          $publishDir = Join-Path $env:GITHUB_WORKSPACE "_publish"
          if (Test-Path $publishDir) { Remove-Item $publishDir -Recurse -Force }
          New-Item -ItemType Directory -Force -Path $publishDir | Out-Null

          # Publish your deployable project (adjust if needed)
          dotnet publish {.main_prj file path from repo root} -c $env:CONFIGURATION --no-build -o $publishDir # eg. src\myapp\myapp.csprj

          Write-Host "Publish output: $publishDir"
          Get-ChildItem $publishDir | Select-Object Name

      - name: Verify deployment repo path
        shell: powershell
        run: |
          $ErrorActionPreference = "Stop"

          if (!(Test-Path $env:DEPLOY_REPO_PATH)) {
            throw "DEPLOY_REPO_PATH not found: $env:DEPLOY_REPO_PATH. Clone the deployment repo there first."
          }
          if (!(Test-Path (Join-Path $env:DEPLOY_REPO_PATH ".git"))) {
            throw "DEPLOY_REPO_PATH is not a git repo: $env:DEPLOY_REPO_PATH"
          }
      
      - name: Update deployment repo www and push PreRelease (HTTPS + PAT)
        shell: powershell
        env:
          DEPLOY_PAT: ${{ secrets.DEPLOY_PAT }}
          DEPLOY_BRANCH: PreRelease
        run: |
          $ErrorActionPreference = "Stop"

          # Hard-disable prompts/UI
          $env:GIT_TERMINAL_PROMPT = "0"
          $env:GCM_INTERACTIVE = "Never"

          $deployRepo = $env:DEPLOY_REPO_PATH
          $branch = $env:DEPLOY_BRANCH
          $publishDir = Join-Path $env:GITHUB_WORKSPACE "_publish"
          $www = Join-Path $deployRepo "www"

          if (!(Test-Path (Join-Path $deployRepo ".git"))) { throw "Not a git repo: $deployRepo" }
          if (!(Test-Path $publishDir)) { throw "Publish folder not found: $publishDir" }

          cd $deployRepo

          git config user.name  "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          # Prevent saving credentials to the VPS
          git config --local credential.helper ""

          # Ensure origin is HTTPS to github.com (no token stored)
          $remote = git remote get-url origin

          function ToHttpsGitHub([string]$url) {
            if ($url -like "https://github.com/*") { return $url }
            if ($url -match "^git@github\.com:(.+)$") { return "https://github.com/" + $Matches[1] }
            return $url
          }

          $cleanRemote = ToHttpsGitHub $remote
          if ($cleanRemote -notlike "https://github.com/*") {
            throw "Origin remote must be GitHub HTTPS/SSH. Current: $remote"
          }

          $authRemote = $cleanRemote -replace "^https://", ("https://x-access-token:" + $env:DEPLOY_PAT + "@")

          # Use auth remote for ALL git network operations
          git remote set-url origin $authRemote
          try {
            Write-Host "== FETCH =="
            git fetch origin $branch

            Write-Host "== CHECKOUT =="
            git checkout -B $branch FETCH_HEAD
            git reset --hard FETCH_HEAD
            git clean -fdx

            Write-Host "== SYNC WWW =="
            if (!(Test-Path $www)) { New-Item -ItemType Directory -Force -Path $www | Out-Null }

            # Clean only www contents
            Get-ChildItem $www -Force | Remove-Item -Recurse -Force

            # Copy published output into www
            Copy-Item -Path (Join-Path $publishDir "*") -Destination $www -Recurse -Force

            # Remove appsettings*.json from www
            Get-ChildItem -Path $www -Filter "appsettings*.json" -File -ErrorAction SilentlyContinue | Remove-Item -Force

            Write-Host "== COMMIT =="
            git add -A
            $status = git status --porcelain
            if ([string]::IsNullOrWhiteSpace($status)) {
              Write-Host "No changes to commit."
              exit 0
            }

            git commit -m "Sync www from $env:GITHUB_REPOSITORY@$env:GITHUB_SHA"

            Write-Host "== PUSH =="
            git push origin $branch
          }
          finally {
            # Restore clean remote URL (no token)
            git remote set-url origin $cleanRemote
          }
      
      - name: Sync publish output to deployment repo www (no appsettings)
        shell: powershell
        run: |
          $ErrorActionPreference = "Stop"

          $deployRepo  = $env:DEPLOY_REPO_PATH
          $www         = Join-Path $deployRepo "www"
          $publishDir  = Join-Path $env:GITHUB_WORKSPACE "_publish"

          if (!(Test-Path $www)) { New-Item -ItemType Directory -Force -Path $www | Out-Null }

          # Clean only www contents (leave docs and example.appsettings.json outside www untouched)
          Get-ChildItem $www -Force | Remove-Item -Recurse -Force

          # Copy published output into www
          Copy-Item -Path (Join-Path $publishDir "*") -Destination $www -Recurse -Force

          # Remove appsettings*.json from www (deployment repo keeps example.appsettings.json outside www)
          Get-ChildItem -Path $www -Filter "appsettings*.json" -File -ErrorAction SilentlyContinue | Remove-Item -Force

      - name: Snapshot PreRelease (keep 5) + push updated PreRelease (HTTPS PAT, no local credential storage)
        shell: powershell
        env:
          DEPLOY_PAT: ${{ secrets.DEPLOY_PAT }}
          DEPLOY_BRANCH: PreRelease
        run: |
          $ErrorActionPreference = "Stop"

          $deployRepo  = $env:DEPLOY_REPO_PATH
          $branch      = $env:DEPLOY_BRANCH
          $keep        = [int]$env:SNAPSHOT_KEEP

          cd $deployRepo
          git push