Retour aux articles

Deploying a .NET Web App on SmarterASP with GitHub (Step-by-Step CI/CD)

Deploying a .NET Web App on SmarterASP with GitHub (Step-by-Step CI/CD)

You're using GitHub to version your ASP.NET Core or Blazor app, and you're hosting on SmarterASP.NET? This guide shows you how to fully automate your deployments with GitHub Actions, without leaving the GitHub ecosystem, without any third-party tools, and without copying a single file by hand.

TL;DR

  • GitHub Actions orchestrates the build and deployment from your repository, on a Windows runner (required for MSDeploy).
  • SmarterASP.NET exposes a WebDeploy (MSDeploy) endpoint on port 8172, we use it directly.
  • Secrets (credentials, passwords) are stored in GitHub Secrets, non-sensitive values in GitHub Variables.
  • -skip rules protect critical server-side files (Let's Encrypt certificates, sitemaps…).

GitHub Actions vs Azure DevOps: which one to pick?

If your code is already on GitHub, GitHub Actions is the natural choice: everything is in one place, the configuration lives in the repository, and the free tier is generous (2,000 minutes/month for private repositories). Azure DevOps makes more sense if you already have a Microsoft ecosystem in place (boards, artifacts, test plans), but for a .NET project hosted on SmarterASP.NET, GitHub Actions gets the job done without friction.

The deployment logic is identical in both cases: dotnet publish produces a folder, msdeploy.exe syncs it to the server. Only the configuration file syntax changes.

ℹ Prerequisites
You need a GitHub account, a repository containing your .NET project, and your SmarterASP.NET credentials (username / password from your control panel). WebDeploy must be enabled in your site settings.
Not a customer yet? My partner link gives you access to a promotional offer.


Workflow anatomy

A GitHub Actions workflow is a YAML file placed in .github/workflows/. It is made up of jobs, which are themselves made up of steps. Here, we do everything in a single build_and_deploy job to keep things simple, the publish output is the direct result of the build, so there's no need to store an intermediate artifact.

push to main
    └── job: build_and_deploy (windows-latest)
            ├── Checkout
            ├── Setup .NET
            ├── Restore
            ├── Build
            ├── Publish
            ├── WebDeploy → SmarterASP.NET
            └── Warm-up

✓ Why windows-latest?
msdeploy.exe is a Windows-only tool. Ubuntu or macOS runners cannot execute it. GitHub provides Windows runners in its free tier, so this is not an issue.


The full annotated workflow

Create the file .github/workflows/deploy.yml in your repository:

name: Build & Deploy to SmarterASP.NET

# Triggers: push to main, or manual run from the GitHub Actions tab
on:
  push:
    branches: [ main ]
  workflow_dispatch:       # Allows manual re-runs from the Actions interface

jobs:
  build_and_deploy:
    runs-on: windows-latest    # Required: msdeploy.exe only exists on Windows
    environment: prod          # GitHub Environment (enables protection rules and approvals)

    steps:
      # 1. Check out the source code
      - name: Checkout
        uses: actions/checkout@v4

      # 2. Install the required .NET SDK
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'   # Adapt to your target version

      # 3. Restore NuGet dependencies
      - name: Restore
        run: dotnet restore src/YourApp/YourApp.csproj

      # 4. Compile in Release mode
      - name: Build
        run: dotnet build src/YourApp/YourApp.csproj --configuration Release --no-restore

      # 5. Publish the application to the ./publish folder
      #    --no-build avoids recompiling what was just built
      - name: Publish
        run: dotnet publish src/YourApp/YourApp.csproj --configuration Release --output publish --no-build

      # 6. Deploy via WebDeploy (MSDeploy) to SmarterASP.NET
      - name: WebDeploy to SmarterASP.NET
        shell: pwsh
        env:
          # Non-sensitive values: stored in GitHub Variables (Settings → Variables)
          WEBSITE_NAME:          ${{ vars.WEBDEPLOY_WEBSITE_NAME }}
          SERVER_COMPUTER_NAME:  ${{ vars.WEBDEPLOY_SERVER_URL }}
          SERVER_USERNAME:       ${{ vars.WEBDEPLOY_USERNAME }}
          PARAM_EXCLUDED_FILES:  ${{ vars.EXCLUDED_FILES }}
          PRESERVE_REMOTE_FILES: ${{ vars.PRESERVE_REMOTE_FILES }}
          # Sensitive secret: stored in GitHub Secrets (Settings → Secrets)
          SERVER_PASSWORD:       ${{ secrets.WEBDEPLOY_PASSWORD }}
        run: |
          # Look for msdeploy at the two standard locations on Windows runners
          $msdeploy = @(
            "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe",
            "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe"
          ) | Where-Object { Test-Path $_ } | Select-Object -First 1

          if (-not $msdeploy) { throw "msdeploy.exe not found on this runner" }
          Write-Host "msdeploy found: $msdeploy"
          Write-Host "Deploying to: $env:WEBSITE_NAME"

          $sourcePath = "$PWD\publish"

          $msdeployArgs = @(
            "-verb:sync",
            "-source:contentPath=$sourcePath",
            "-dest:contentPath=$env:WEBSITE_NAME,computerName=$env:SERVER_COMPUTER_NAME,userName=$env:SERVER_USERNAME,password=$env:SERVER_PASSWORD,authtype=Basic,includeAcls=False",
            "-allowUntrusted",                  # Useful if the certificate chain causes issues on the runner
            "-disableLink:AppPoolExtension",
            "-disableLink:ContentExtension",
            "-disableLink:CertificateExtension",
            "-enableRule:AppOffline"            # Places an app_offline.htm during deployment
          )

          # Option: do not delete unknown files on the remote server
          if ($env:PRESERVE_REMOTE_FILES -eq "true") {
            $msdeployArgs += "-enableRule:DoNotDeleteRule"
          }

          # --- Custom exclusions (via EXCLUDED_FILES variable) ---
          $env:PARAM_EXCLUDED_FILES -split ";" | Where-Object { $_.Trim() -ne "" } | ForEach-Object {
            $regex = "(?i).*$([Regex]::Escape($_.Trim()))$"
            Write-Host "Excluding file: $regex"
            $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=$regex"
          }

          # --- Protect critical server-side folders ---

          # Let's Encrypt certificates (ACME challenge folder)
          $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=(?i).*\\\.well-known.*"

          # Dynamically generated sitemaps written by the app at runtime
          $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=(?i).*\\wwwroot\\sitemaps(`$|\\.*)"
          $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=(?i).*\\wwwroot\\sitemaps\\.*"

          Write-Host "Running msdeploy..."
          & $msdeploy @msdeployArgs

          # Check exit code PowerShell does not always propagate errors automatically
          if ($LASTEXITCODE -ne 0) {
            Write-Error "msdeploy failed with exit code $LASTEXITCODE"
            exit $LASTEXITCODE
          }

      # 7. Warm-up: send an HTTP request to wake the app from its cold start
      - name: Application warm-up
        shell: pwsh
        continue-on-error: true    # A failed warm-up should not invalidate a successful deployment
        run: |
          $url = "${{ vars.WEBSITE_URL }}"
          $maxRetries = 3
          $retryDelay = 10
          Write-Host "Warming up $url..."
          for ($i = 1; $i -le $maxRetries; $i++) {
            try {
              $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 45
              Write-Host "Site responded: $($r.StatusCode) - OK"
              break
            } catch {
              Write-Host "Attempt $i/$maxRetries failed: $_"
              if ($i -lt $maxRetries) { Start-Sleep -Seconds $retryDelay }
            }
          }

Variables and secrets to configure in GitHub

Go to Settings → Secrets and variables → Actions in your repository. GitHub distinguishes two categories:

  • Secrets: encrypted, never shown in logs, accessed via ${{ secrets.NAME }}
  • Variables: visible in the interface, accessed via ${{ vars.NAME }}
Name Type Example value
WEBDEPLOY_WEBSITE_NAME Variable mysite.com
WEBDEPLOY_SERVER_URL Variable https://win1234.site4now.net:8172/msdeploy.axd?site=mysite.com
WEBDEPLOY_USERNAME Variable your-smarterasp-login
WEBDEPLOY_PASSWORD Secret •••••••••
WEBSITE_URL Variable https://mysite.com
EXCLUDED_FILES Variable appsettings.Production.json;Web.config
PRESERVE_REMOTE_FILES Variable false

✓ Tip
Find your WEBDEPLOY_SERVER_URL in the SmarterASP.NET control panel → Publish your websiteConnection tab. It always starts with https://winXXXX.site4now.net:8172.


The prod environment and protection rules

The workflow references environment: prod. To create this environment in GitHub, go to Settings → Environments → New environment and name it prod.

The benefit? You can add:

  • Required reviewers, a collaborator must approve before the deployment runs.
  • A wait timer, for example, 5 minutes between the push and the actual deployment.
  • Allowed branches, only the main branch can trigger a deployment to prod.

This is the equivalent of Azure DevOps Environment approvals, built directly into GitHub.


Step by step: getting it up and running

  1. Enable WebDeploy in the SmarterASP.NET control panel. Verify that port 8172 is accessible from the outside.

  2. Create the file .github/workflows/deploy.yml at the root of your repository. Copy the YAML above and adapt the path to your .csproj and the .NET version.

  3. Add secrets and variables under Settings → Secrets and variables → Actions in your GitHub repository.

  4. Create the prod environment under Settings → Environments. Add a protection rule if you work in a team.

  5. Push to main and watch the Actions tab of your repository. Each step shows its logs in real time.

  6. If a step fails, click on it to see the error message. msdeploy is usually very explicit (wrong credentials, closed port, etc.).


Common pitfalls

"msdeploy not found"

GitHub's windows-latest runners include Web Deploy V3. The script checks both standard locations. If it still fails, add an installation step before deployment:

- name: Install WebDeploy
  run: choco install webdeploy -y
  shell: pwsh

My appsettings.Production.json gets overwritten

Add it to the EXCLUDED_FILES variable: appsettings.Production.json. The PowerShell script converts it into a -skip rule that protects the file on the server on every deployment.

⚠ Warning
Never version production secrets in appsettings.json. Use appsettings.Production.json outside your repository, or GitHub Secrets with runtime environment variable injection.

The warm-up fails with a 302 or an SSL error

Expected if your site redirects HTTP → HTTPS. The continue-on-error: true on the warm-up step means GitHub does not treat this as a workflow failure. The deployment is complete regardless.

Difference from Azure DevOps: no intermediate artifact

In the Azure DevOps pipeline from the previous article, an artifact was published between the Build stage and the Deploy stage. Here, since everything runs in a single job, the ./publish folder is directly available to the next step. This is simpler, but it also means you can't reuse that artifact for a second environment without restructuring the workflow into multiple jobs.


Going further

Split build and deploy into two jobs

If you want to add automated tests or deploy to multiple environments from the same artifact, split the workflow into two jobs: a build job that uploads an artifact via actions/upload-artifact, and a deploy job that downloads it via actions/download-artifact. This is the equivalent of the multi-stage structure in Azure DevOps.

Trigger only on specific paths

To avoid redeploying when only a documentation file changes:

on:
  push:
    branches: [ main ]
    paths:
      - 'src/**'
      - '.github/workflows/deploy.yml'

Notify on failure

Add a conditional step at the end of the workflow:

- name: Notify on failure
  if: failure()
  shell: pwsh
  run: |
    # Call a Teams, Slack, or other webhook
    Invoke-RestMethod -Uri "${{ secrets.SLACK_WEBHOOK }}" -Method Post -Body '{"text":"🚨 Deployment failed on main"}'

✓ Final best practice
Enable branch protection rules on main under Settings → Branches. Require pull requests to go through a review before merging. Combined with the prod environment approval, you get a solid deployment workflow without any external tooling.


Conclusion

GitHub Actions makes continuous deployment to SmarterASP.NET achievable in under an hour of configuration. Everything lives in your repository, the configuration is versioned, and the deployment history is visible directly in the Actions tab. It's a serious alternative to Azure DevOps for projects that don't need the full Microsoft ecosystem.

If you've already read the Azure DevOps Pipelines article, you'll notice that the PowerShell/MSDeploy logic is almost identical, only the YAML wrapper changes. That's intentional: once you understand WebDeploy, you can switch from one CI/CD tool to another without relearning the deployment mechanism.

Questions about EF Core migrations at deploy time, multi-environment setups, or GitHub Environments? Drop them in the comments.

ASP.NET Hosting - SmarterASP.NET

Commentaires (0)

Aucun commentaire pour le moment. Soyez le premier à commenter !

Laisser un commentaire

Votre email ne sera pas publié.

© 2026 Pierre-Henri

Tous droits réservés.

Projects
Privacy Policy

Développé avec et .NET

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.