{"id":81612,"date":"2026-08-21T11:47:55","date_gmt":"2026-08-21T06:17:55","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=81612"},"modified":"2026-09-01T15:53:18","modified_gmt":"2026-09-01T10:23:18","slug":"automating-asp-net-core-deployments-with-github-actions-a-practical-ci-cd-guide","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/automating-asp-net-core-deployments-with-github-actions-a-practical-ci-cd-guide\/","title":{"rendered":"Automating ASP.NET Core Deployments with GitHub Actions: A Practical CI\/CD Guide"},"content":{"rendered":"<p>\ud83d\ude80 As developers, we&#8217;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.<\/p>\n<p><strong>\ud83d\udd04 This is where CI\/CD comes in.<\/strong><\/p>\n<p><strong>CI\/CD (Continuous Integration and Continuous Deployment)<\/strong> helps automate the software delivery process, allowing teams to build, test, and deploy applications with confidence.<\/p>\n<p>In this blog, I&#8217;ll walk through how to set up a simple CI\/CD pipeline for an ASP.NET Core application using GitHub Actions.<\/p>\n<h2>\ud83d\udd04 What is CI\/CD?<\/h2>\n<p>Before jumping into the implementation, let&#8217;s briefly understand the concepts.<\/p>\n<p><strong>Continuous Integration (CI)<\/strong> 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&#8217;t break existing functionality.<\/p>\n<p><strong>Continuous Deployment (CD)<\/strong> takes things a step further by automatically deploying validated code to an environment such as staging or production.<\/p>\n<p>\ud83d\udccc The typical workflow looks like this:<\/p>\n<pre>\ud83d\udc68\u200d\ud83d\udcbb Developer\r\n       \u2193\r\n\ud83d\udcc2 GitHub Repository\r\n       \u2193\r\n\u26a1 GitHub Actions\r\n       \u2193\r\n\ud83d\udd28 Build\r\n       \u2193  \r\n\ud83e\uddea Test\r\n       \u2193\r\n\ud83d\udce6 Publish\r\n       \u2193\r\n\u2601\ufe0f Deploy to Staging\r\n       \u2193\r\n\u2705 Approval\r\n       \u2193\r\n\ud83d\ude80 Production<\/pre>\n<p>With this approach, deployments become faster, safer, and more reliable.<\/p>\n<h2>\u2699\ufe0f Why GitHub Actions?<\/h2>\n<p>GitHub Actions is integrated directly into GitHub and uses YAML files to define workflows.<\/p>\n<p>Key benefits include:<\/p>\n<ul>\n<li>\ud83d\udd17 Native GitHub integration<\/li>\n<li>\ud83d\udcdd Easy YAML-based configuration<\/li>\n<li>\ud83d\udcbb Support for Windows, Linux, and macOS runners<\/li>\n<li>\ud83d\udd10 Built-in secret management<\/li>\n<li>\ud83e\udde9 Extensive marketplace of reusable actions<\/li>\n<\/ul>\n<p>For most .NET projects, GitHub Actions provides everything needed to automate the delivery pipeline.<\/p>\n<h2>\ud83d\udee0\ufe0f Creating the Workflow<\/h2>\n<p>GitHub Actions workflows are stored inside the repository under:<\/p>\n<pre>.github\/workflows<\/pre>\n<p>Let&#8217;s create a file called:<\/p>\n<pre>dotnet-ci.yml<\/pre>\n<p>A typical ASP.NET Core solution may look something like this:<\/p>\n<p>MyApplication\/<br \/>\n\u2502<br \/>\n\u251c\u2500\u2500 src\/<br \/>\n\u2502 \u251c\u2500\u2500 MyApplication.Web\/<br \/>\n\u2502 \u2502 \u2514\u2500\u2500 MyApplication.Web.csproj<br \/>\n\u2502 \u2502<br \/>\n\u2502 \u251c\u2500\u2500 MyApplication.Core\/<br \/>\n\u2502 \u2502 \u2514\u2500\u2500 MyApplication.Core.csproj<br \/>\n\u2502 \u2502<br \/>\n\u2502 \u2514\u2500\u2500 MyApplication.Infrastructure\/<br \/>\n\u2502 \u2514\u2500\u2500 MyApplication.Infrastructure.csproj<br \/>\n\u2502<br \/>\n\u251c\u2500\u2500 tests\/<br \/>\n\u2502 \u2514\u2500\u2500 MyApplication.Tests\/<br \/>\n\u2502 \u2514\u2500\u2500 MyApplication.Tests.csproj<br \/>\n\u2502<br \/>\n\u251c\u2500\u2500 MyApplication.sln<br \/>\n\u2514\u2500\u2500 .github\/<br \/>\n\u2514\u2500\u2500 workflows\/<br \/>\n\u2514\u2500\u2500 dotnet-ci.yml<\/p>\n<p>Using the solution file is usually better than relying on the current directory because the solution can contain multiple projects.<\/p>\n<h2>\ud83d\udd28 Basic CI Workflow<\/h2>\n<p>The following workflow runs whenever code is pushed to the <strong>main<\/strong> branch:<\/p>\n<pre>name: ASP.NET Core CI\r\n\r\non:\r\n push:\r\n   branches:\r\n     - main\r\n\r\njobs:\r\n  build:\r\n    runs-on: ubuntu-latest\r\n\r\n    steps:\r\n      - name: Checkout Source\r\n        uses: actions\/checkout@v4\r\n\r\n      - name: Setup .NET\r\n        uses: actions\/setup-dotnet@v4\r\n        with:\r\n          dotnet-version: '9.0.x'\r\n\r\n      - name: Restore Dependencies\r\n        run: dotnet restore MyApplication.sln\r\n\r\n      - name: Build Application\r\n        run: dotnet build MyApplication.sln --configuration Release --no-restore\r\n\r\n      - name: Run Tests\r\n        run: dotnet test MyApplication.sln --configuration Release --no-build<\/pre>\n<h2>\ud83d\udd0d What is happening here?<\/h2>\n<h3>Let&#8217;s break the workflow down.<\/h3>\n<h3>1. Checkout Source<\/h3>\n<pre>- name: Checkout Source\r\n  uses: actions\/checkout@v4<\/pre>\n<p>GitHub Actions runners start with a clean environment. This <strong>checkout<\/strong> action downloads your repository code so the workflow can work with it.<\/p>\n<h3>2. Set up .NET<\/h3>\n<pre>- name: Setup .NET\r\n  uses: actions\/setup-dotnet@v4\r\n  with:\r\n    dotnet-version: '9.0.x'<\/pre>\n<p>This installs the required .NET SDK on the GitHub Actions runner.<\/p>\n<p><em><strong>Make sure this version matches the framework your application uses.<\/strong><\/em><\/p>\n<p>For example:<\/p>\n<p style=\"padding-left: 40px;\"><strong>.NET 8 \u2192 8.0.x<\/strong><br \/>\n<strong>.NET 9 \u2192 9.0.x<\/strong><\/p>\n<h3>3. Restore Dependencies<\/h3>\n<pre>- name: Restore Dependencies\r\n  run: dotnet restore MyApplication.sln<\/pre>\n<p>This restores all NuGet packages required by the projects in the solution.<\/p>\n<h3>4. Build Application<\/h3>\n<pre>- name: Build Application\r\n  run: dotnet build MyApplication.sln --configuration Release --no-restore<\/pre>\n<p>Here we explicitly provide the solution path.<\/p>\n<p>The &#8211;<strong>-configuration Release<\/strong> option creates a Release build, while &#8211;no-restore prevents NuGet packages from being restored again because we already restored them in the previous step.<\/p>\n<h3>5. Run Tests<\/h3>\n<pre>- name: Run Tests\r\n  run: dotnet test MyApplication.sln --configuration Release --no-build<\/pre>\n<p>This executes the automated tests in the solution.<\/p>\n<p>The &#8216;<strong>&#8211;no-build&#8217;<\/strong> option avoids building the solution again because it was already built successfully in the previous step.<\/p>\n<p>This keeps the pipeline more efficient.<\/p>\n<h2>\ud83e\uddea Adding Automated Testing<\/h2>\n<p>One of the biggest benefits of CI is preventing broken code from reaching production.<\/p>\n<p>Imagine a developer accidentally introduces a bug into a critical service. Without automated testing, the issue might only be discovered after deployment.<\/p>\n<p>By including the following step:<\/p>\n<pre>- name: Run Tests\r\n  run: dotnet test MyApplication.sln --configuration Release --no-build\r\n\r\n<\/pre>\n<p>every commit is validated automatically.<\/p>\n<p>This gives the team immediate feedback whenever a change introduces a failure.<\/p>\n<h2>\ud83d\udce6 Publishing the Application<\/h2>\n<p>After a successful build and test execution, the next step is creating a deployable package.<\/p>\n<p>We can publish the ASP.NET Core application using:<\/p>\n<pre>- name: Publish Application\r\n  run: dotnet publish src\/MyApplication.Web\/MyApplication.Web.csproj --configuration Release --output .\/publish<\/pre>\n<p>The generated files are placed inside the <strong>publish<\/strong> folder and can be deployed to a server, Docker container, or cloud platform.<\/p>\n<h2>\ud83d\udce6 Uploading the Build Artifact<\/h2>\n<p>Instead of deploying the published files directly, we can first store them as a GitHub Actions artifact.<\/p>\n<pre>- name: Upload Artifact\r\n  uses: actions\/upload-artifact@v4\r\n  with:\r\n    name: myapplication\r\n    path: .\/publish<\/pre>\n<p>Now the published application is available as a workflow artifact.<\/p>\n<p>Artifacts are especially useful when CI and CD are separated into different jobs or workflows.<\/p>\n<p>A common delivery flow is:<\/p>\n<pre>Build\r\n  \u2193 \r\nTest\r\n  \u2193\r\nPublish\r\n  \u2193 \r\nArtifact\r\n  \u2193\r\nStaging\r\n  \u2193\r\nProduction<\/pre>\n<h2>\u2601\ufe0f Deploying to Azure<\/h2>\n<p>For teams hosting applications in Azure App Service, deployment can be fully automated.<\/p>\n<p>First, store the Azure publish profile in GitHub Secrets.<\/p>\n<p>Then add a deployment step:<\/p>\n<pre>- name: Deploy to Azure\r\n  uses: azure\/webapps-deploy@v3\r\n  with:\r\n    app-name: ${{ secrets.AZURE_WEBAPP_NAME }}\r\n    publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}\r\n    package: publish<\/pre>\n<p>Now the published application can be deployed automatically after the CI process succeeds.<\/p>\n<p>For production workloads, teams should also consider stronger authentication approaches such as federated credentials\/OIDC rather than relying on long-lived deployment credentials.<\/p>\n<h2>\ud83d\udd10 Protecting Sensitive Information<\/h2>\n<p>A common mistake is hardcoding credentials directly in workflow files.<\/p>\n<p>For example:<\/p>\n<pre>password: MyPassword123<\/pre>\n<p>This should never be done.<\/p>\n<p>Instead, store sensitive values inside GitHub Secrets and reference them securely:<\/p>\n<pre>${{ secrets.DB_PASSWORD }}<\/pre>\n<p>You can create secrets from:<\/p>\n<p><strong>GitHub \u2192 Repository \u2192 Settings \u2192 Secrets and variables \u2192 Actions<\/strong><\/p>\n<p>Examples of values that should generally be stored as secrets include:<\/p>\n<pre>AZURE_WEBAPP_NAME\r\nAZURE_PUBLISH_PROFILE\r\nDB_PASSWORD\r\nAPI_KEY\r\nCONNECTION_STRING<\/pre>\n<p>This keeps sensitive information out of your source code.<\/p>\n<h2>\ud83c\udf0d Using Multiple Environments<\/h2>\n<p>For a production application, deploying every commit directly to production may not be the best approach.<\/p>\n<p>A more controlled setup could look like:<\/p>\n<pre>Developer\r\n  \u2193\r\nGitHub\r\n  \u2193\r\nBuild\r\n  \u2193\r\nTests\r\n  \u2193\r\nStaging\r\n  \u2193\r\nManual Approval\r\n  \u2193\r\nProduction<\/pre>\n<p>You can maintain separate environments such as:<\/p>\n<ul>\n<li>Development<\/li>\n<li>Testing<\/li>\n<li>Staging<\/li>\n<li>Production<\/li>\n<\/ul>\n<p>GitHub environments can also be used to control secrets and require approval before production deployment.<\/p>\n<h2>\u26a1 Improving the Pipeline<\/h2>\n<p>Once the basic pipeline is working, there are several ways to improve it.<\/p>\n<h2>\u26a1 Cache NuGet Packages<\/h2>\n<p>Installing dependencies on every workflow run can take time.<\/p>\n<p><strong>actions\/setup-dotnet<\/strong> can be configured with NuGet caching:<\/p>\n<pre>- name: Setup .NET\r\n  uses: actions\/setup-dotnet@v4\r\n  with:\r\n    dotnet-version: '9.0.x'\r\n    cache: true\r\n    cache-dependency-path: '**\/packages.lock.json'<\/pre>\n<p>This can reduce workflow execution time when dependencies haven&#8217;t changed.<\/p>\n<h2>\ud83d\udd12 Protect the Main Branch<\/h2>\n<p>Configure branch protection rules so that pull requests must pass CI checks before they can be merged.<\/p>\n<p>This prevents code that fails the pipeline from being merged into <strong>main<\/strong>.<\/p>\n<h2>\ud83d\udc40 Require Production Approval<\/h2>\n<p>For production deployments, consider requiring manual approval.<\/p>\n<p>This gives the team one final checkpoint before changes reach production.<\/p>\n<h2>\ud83d\udcca Monitor Pipeline Health<\/h2>\n<p>Treat pipeline failures as high-priority issues.<\/p>\n<p>A failing pipeline can indicate:<\/p>\n<ul>\n<li>Build failures<\/li>\n<li>Broken tests<\/li>\n<li>Dependency problems<\/li>\n<li>Configuration issues<\/li>\n<li>Deployment failures<\/li>\n<\/ul>\n<p>A healthy pipeline should provide fast and reliable feedback to developers.<\/p>\n<h2>\ud83e\udde9 Complete CI Workflow<\/h2>\n<p>Putting everything together, our CI workflow now looks like this:<\/p>\n<pre>name: ASP.NET Core CI\r\n\r\non:\r\n  push:\r\n    branches:\r\n      - main\r\n\r\njobs:\r\n  build:\r\n    runs-on: ubuntu-latest\r\n\r\n    steps:\r\n\r\n      - name: Checkout Source\r\n        uses: actions\/checkout@v4\r\n\r\n      - name: Setup .NET\r\n        uses: actions\/setup-dotnet@v4\r\n        with:\r\n          dotnet-version: '9.0.x'\r\n\r\n      - name: Restore Dependencies\r\n        run: dotnet restore MyApplication.sln\r\n\r\n      - name: Build Application\r\n        run: dotnet build MyApplication.sln --configuration Release --no-restore\r\n\r\n      - name: Run Tests\r\n        run: dotnet test MyApplication.sln --configuration Release --no-build\r\n\r\n      - name: Publish Application\r\n        run: dotnet publish src\/MyApplication.Web\/MyApplication.Web.csproj \\\r\n             --configuration Release \\\r\n             --output .\/publish\r\n\r\n      - name: Upload Artifact\r\n        uses: actions\/upload-artifact@v4\r\n        with:\r\n          name: myapplication\r\n          path: .\/publish\r\n<\/pre>\n<p><strong>This gives us a simple but practical CI pipeline:<\/strong><\/p>\n<pre>\ud83d\udce5 Checkout\r\n      \u2193\r\n\u2699\ufe0f Setup .NET\r\n      \u2193\r\n\ud83d\udce6 Restore\r\n      \u2193\r\n\ud83d\udd28 Build\r\n      \u2193\r\n\ud83e\uddea Test\r\n      \u2193\r\n\ud83d\udce6 Publish\r\n      \u2193\r\n\u2601\ufe0f Upload Artifact<\/pre>\n<p>From here, the artifact can be consumed by a separate deployment job or workflow.<\/p>\n<h2>\ud83d\ude80 Final Thoughts<\/h2>\n<p><strong>CI\/CD<\/strong> is no longer a luxury for modern software teams\u2014it\u2019s a necessity.<\/p>\n<p>By automating builds, tests, and deployments with GitHub Actions, .NET developers can spend less time on repetitive tasks and more time building valuable features.<\/p>\n<p>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.<\/p>\n<p><strong>Build it once. Automate it. Deploy with confidence.<\/strong> \ud83d\ude80<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\ude80 As developers, we&#8217;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. \ud83d\udd04 This is where CI\/CD comes in. CI\/CD (Continuous Integration and Continuous Deployment) helps [&hellip;]<\/p>\n","protected":false},"author":1266,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":14},"categories":[5867],"tags":[1853,4252,1892,5627],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/81612"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/1266"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=81612"}],"version-history":[{"count":18,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/81612\/revisions"}],"predecessor-version":[{"id":82004,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/81612\/revisions\/82004"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=81612"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=81612"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=81612"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}