π 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. π