Automating ASP.NET Core Deployments with GitHub Actions: A Practical CI/CD Guide

8 min read
Share:

πŸš€ As developers, we’ve all experienced the frustration of manual deployments. You finish a feature, run tests locally, create a build, copy files to a server, and hope everything works in production. The process is time-consuming, repetitive, and prone to mistakes.

πŸ”„ This is where CI/CD comes in.

CI/CD (Continuous Integration and Continuous Deployment) helps automate the software delivery process, allowing teams to build, test, and deploy applications with confidence.

In this blog, I’ll walk through how to set up a simple CI/CD pipeline for an ASP.NET Core application using GitHub Actions.

πŸ”„ What is CI/CD?

Before jumping into the implementation, let’s briefly understand the concepts.

Continuous Integration (CI) is the practice of automatically building and testing your application whenever code is pushed to the repository. This helps catch issues early and ensures that new changes don’t break existing functionality.

Continuous Deployment (CD) takes things a step further by automatically deploying validated code to an environment such as staging or production.

πŸ“Œ The typical workflow looks like this:

πŸ‘¨β€πŸ’» Developer
       ↓
πŸ“‚ GitHub Repository
       ↓
⚑ GitHub Actions
       ↓
πŸ”¨ Build
       ↓  
πŸ§ͺ Test
       ↓
πŸ“¦ Publish
       ↓
☁️ Deploy to Staging
       ↓
βœ… Approval
       ↓
πŸš€ Production

With this approach, deployments become faster, safer, and more reliable.

βš™οΈ Why GitHub Actions?

GitHub Actions is integrated directly into GitHub and uses YAML files to define workflows.

Key benefits include:

  • πŸ”— Native GitHub integration
  • πŸ“ Easy YAML-based configuration
  • πŸ’» Support for Windows, Linux, and macOS runners
  • πŸ” Built-in secret management
  • 🧩 Extensive marketplace of reusable actions

For most .NET projects, GitHub Actions provides everything needed to automate the delivery pipeline.

πŸ› οΈ Creating the Workflow

GitHub Actions workflows are stored inside the repository under:

.github/workflows

Let’s create a file called:

dotnet-ci.yml

A typical ASP.NET Core solution may look something like this:

MyApplication/
β”‚
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ MyApplication.Web/
β”‚ β”‚ └── MyApplication.Web.csproj
β”‚ β”‚
β”‚ β”œβ”€β”€ MyApplication.Core/
β”‚ β”‚ └── MyApplication.Core.csproj
β”‚ β”‚
β”‚ └── MyApplication.Infrastructure/
β”‚ └── MyApplication.Infrastructure.csproj
β”‚
β”œβ”€β”€ tests/
β”‚ └── MyApplication.Tests/
β”‚ └── MyApplication.Tests.csproj
β”‚
β”œβ”€β”€ MyApplication.sln
└── .github/
└── workflows/
└── dotnet-ci.yml

Using the solution file is usually better than relying on the current directory because the solution can contain multiple projects.

πŸ”¨ Basic CI Workflow

The following workflow runs whenever code is pushed to the main branch:

name: ASP.NET Core CI

on:
 push:
   branches:
     - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Source
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restore Dependencies
        run: dotnet restore MyApplication.sln

      - name: Build Application
        run: dotnet build MyApplication.sln --configuration Release --no-restore

      - name: Run Tests
        run: dotnet test MyApplication.sln --configuration Release --no-build

πŸ” What is happening here?

Let’s break the workflow down.

1. Checkout Source

- name: Checkout Source
  uses: actions/checkout@v4

GitHub Actions runners start with a clean environment. This checkout action downloads your repository code so the workflow can work with it.

2. Set up .NET

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '9.0.x'

This installs the required .NET SDK on the GitHub Actions runner.

Make sure this version matches the framework your application uses.

For example:

.NET 8 β†’ 8.0.x
.NET 9 β†’ 9.0.x

3. Restore Dependencies

- name: Restore Dependencies
  run: dotnet restore MyApplication.sln

This restores all NuGet packages required by the projects in the solution.

4. Build Application

- name: Build Application
  run: dotnet build MyApplication.sln --configuration Release --no-restore

Here we explicitly provide the solution path.

The –-configuration Release option creates a Release build, while –no-restore prevents NuGet packages from being restored again because we already restored them in the previous step.

5. Run Tests

- name: Run Tests
  run: dotnet test MyApplication.sln --configuration Release --no-build

This executes the automated tests in the solution.

The ‘–no-build’ option avoids building the solution again because it was already built successfully in the previous step.

This keeps the pipeline more efficient.

πŸ§ͺ Adding Automated Testing

One of the biggest benefits of CI is preventing broken code from reaching production.

Imagine a developer accidentally introduces a bug into a critical service. Without automated testing, the issue might only be discovered after deployment.

By including the following step:

- name: Run Tests
  run: dotnet test MyApplication.sln --configuration Release --no-build

every commit is validated automatically.

This gives the team immediate feedback whenever a change introduces a failure.

πŸ“¦ Publishing the Application

After a successful build and test execution, the next step is creating a deployable package.

We can publish the ASP.NET Core application using:

- name: Publish Application
  run: dotnet publish src/MyApplication.Web/MyApplication.Web.csproj --configuration Release --output ./publish

The generated files are placed inside the publish folder and can be deployed to a server, Docker container, or cloud platform.

πŸ“¦ Uploading the Build Artifact

Instead of deploying the published files directly, we can first store them as a GitHub Actions artifact.

- name: Upload Artifact
  uses: actions/upload-artifact@v4
  with:
    name: myapplication
    path: ./publish

Now the published application is available as a workflow artifact.

Artifacts are especially useful when CI and CD are separated into different jobs or workflows.

A common delivery flow is:

Build
  ↓ 
Test
  ↓
Publish
  ↓ 
Artifact
  ↓
Staging
  ↓
Production

☁️ Deploying to Azure

For teams hosting applications in Azure App Service, deployment can be fully automated.

First, store the Azure publish profile in GitHub Secrets.

Then add a deployment step:

- name: Deploy to Azure
  uses: azure/webapps-deploy@v3
  with:
    app-name: ${{ secrets.AZURE_WEBAPP_NAME }}
    publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
    package: publish

Now the published application can be deployed automatically after the CI process succeeds.

For production workloads, teams should also consider stronger authentication approaches such as federated credentials/OIDC rather than relying on long-lived deployment credentials.

πŸ” Protecting Sensitive Information

A common mistake is hardcoding credentials directly in workflow files.

For example:

password: MyPassword123

This should never be done.

Instead, store sensitive values inside GitHub Secrets and reference them securely:

${{ secrets.DB_PASSWORD }}

You can create secrets from:

GitHub β†’ Repository β†’ Settings β†’ Secrets and variables β†’ Actions

Examples of values that should generally be stored as secrets include:

AZURE_WEBAPP_NAME
AZURE_PUBLISH_PROFILE
DB_PASSWORD
API_KEY
CONNECTION_STRING

This keeps sensitive information out of your source code.

🌍 Using Multiple Environments

For a production application, deploying every commit directly to production may not be the best approach.

A more controlled setup could look like:

Developer
  ↓
GitHub
  ↓
Build
  ↓
Tests
  ↓
Staging
  ↓
Manual Approval
  ↓
Production

You can maintain separate environments such as:

  • Development
  • Testing
  • Staging
  • Production

GitHub environments can also be used to control secrets and require approval before production deployment.

⚑ Improving the Pipeline

Once the basic pipeline is working, there are several ways to improve it.

⚑ Cache NuGet Packages

Installing dependencies on every workflow run can take time.

actions/setup-dotnet can be configured with NuGet caching:

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '9.0.x'
    cache: true
    cache-dependency-path: '**/packages.lock.json'

This can reduce workflow execution time when dependencies haven’t changed.

πŸ”’ Protect the Main Branch

Configure branch protection rules so that pull requests must pass CI checks before they can be merged.

This prevents code that fails the pipeline from being merged into main.

πŸ‘€ Require Production Approval

For production deployments, consider requiring manual approval.

This gives the team one final checkpoint before changes reach production.

πŸ“Š Monitor Pipeline Health

Treat pipeline failures as high-priority issues.

A failing pipeline can indicate:

  • Build failures
  • Broken tests
  • Dependency problems
  • Configuration issues
  • Deployment failures

A healthy pipeline should provide fast and reliable feedback to developers.

🧩 Complete CI Workflow

Putting everything together, our CI workflow now looks like this:

name: ASP.NET Core CI

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:

      - name: Checkout Source
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restore Dependencies
        run: dotnet restore MyApplication.sln

      - name: Build Application
        run: dotnet build MyApplication.sln --configuration Release --no-restore

      - name: Run Tests
        run: dotnet test MyApplication.sln --configuration Release --no-build

      - name: Publish Application
        run: dotnet publish src/MyApplication.Web/MyApplication.Web.csproj \
             --configuration Release \
             --output ./publish

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: myapplication
          path: ./publish

This gives us a simple but practical CI pipeline:

πŸ“₯ Checkout
      ↓
βš™οΈ Setup .NET
      ↓
πŸ“¦ Restore
      ↓
πŸ”¨ Build
      ↓
πŸ§ͺ Test
      ↓
πŸ“¦ Publish
      ↓
☁️ Upload Artifact

From here, the artifact can be consumed by a separate deployment job or workflow.

πŸš€ Final Thoughts

CI/CD is no longer a luxury for modern software teamsβ€”it’s a necessity.

By automating builds, tests, and deployments with GitHub Actions, .NET developers can spend less time on repetitive tasks and more time building valuable features.

The initial setup may take an hour or two, but the long-term benefits are substantial. Faster releases, fewer deployment mistakes, and greater confidence in production changes make CI/CD one of the highest-value improvements a team can implement.

Build it once. Automate it. Deploy with confidence. πŸš€

Leave a Reply

Your email address will not be published. Required fields are marked *