RO EN

Professional CI/CD with GitHub Actions (1): the anatomy of a serious pipeline

Professional CI/CD with GitHub Actions (1): the anatomy of a serious pipeline ✨ Imagine generată cu AI
Doru Bulubasa
31 August 2026
27 views

Many developers confidently say they have "CI/CD" because they have a workflow that runs a build on every push. It sounds something like this:

I put a GitHub Actions that does dotnet build when I push. So I have CI/CD.

Such a workflow is a good start, but it is nowhere near professional CI/CD. It’s just an automated build.

The difference between "I have a workflow" and "I have a serious pipeline" is seen exactly when something goes wrong: a test that should have blocked the merge, a deploy that fails over clients during peak hours, a secret forgotten in code, or a binary that reaches production different from the one you tested.

In this series, we build, step by step, a professional pipeline for the .NET + Azure stack: separate environments with approvals, infrastructure as code, zero-downtime deploy, and secrets without passwords. This article lays the foundation – what a serious pipeline actually means and what its skeleton looks like.


🎯 What CI/CD really means

The two terms are always stuck together, but they solve different problems.

CI – Continuous Integration

Every code change (push or pull request) automatically triggers a build and a suite of tests. The goal: to catch problems while they are small and cheap, not two weeks later in production.

CD – Continuous Delivery / Deployment

The artifact resulting from CI is automatically delivered further, to real environments. Here is an important distinction:

  • Continuous Delivery: the pipeline prepares everything for deploy, but the step to production requires human approval.

  • Continuous Deployment: if all checks pass, it automatically reaches production, without manual intervention.

For most teams, the healthy option is Delivery: you automate everything but keep an approval button before prod. That’s exactly what we build in article 2.


⚙️ Why GitHub Actions

If you are already on GitHub, Actions is the option with the least friction:

  • The pipeline lives in the repo, as YAML files in .github/workflows.

  • You have ready-made runners (Linux, Windows, macOS), without managing servers yourself.

  • Marketplace with reusable actions for almost anything.

  • Native environments, secrets, and approval gates – no need for external tools.

The rest of the series assumes you work in a GitHub repo with a .NET application and an Azure subscription.


🧩 Anatomy of a workflow

A GitHub Actions workflow has three levels worth keeping clear in your mind:

  • Workflow – the entire YAML file, triggered by an event.

  • Job – a group of steps that run on a runner. Jobs can run in parallel or sequentially.

  • Step – a single action: either a command (run) or a reusable action (uses).

Everything starts from a trigger – the on section. Here is the simplest useful workflow:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Build
        run: dotnet build --configuration Release

What happens here, briefly: on every push or pull request on main, GitHub starts an Ubuntu runner, clones your code (checkout), installs the .NET SDK, and runs the build. That’s it – but it’s already a functional CI.


🏗️ Basic pipeline: build → test → publish

A build without tests doesn’t give you confidence. And a build that doesn’t produce an artifact has nothing to deliver further. Let’s extend the workflow to do all three things:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build --verbosity normal

      - name: Publish
        run: dotnet publish ./src/Api/Api.csproj -c Release -o ./publish

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

Why the steps in this order

  • Separate Restore – downloads NuGet packages once; the next steps reuse the result.

  • Build with --no-restore – doesn’t redo restore, so it’s faster and more predictable.

  • Test with --no-build – runs tests on already compiled binaries, doesn’t recompile. If a test fails, the job fails – and on a pull request this blocks the merge.

  • Publish – produces the ready-to-run output (dlls, config, dependencies) in a folder.

  • Upload artifact – uploads that folder as the run artifact, so it can be taken later by deploy jobs.

Note that build and testing are in a single job, sequentially – because each step depends on the result of the previous one. When we add deploy, that will be a separate job, which waits for this job and downloads the artifact.


💾 Caching: don’t re-download NuGet on every run

A pipeline that downloads all NuGet packages from scratch on every run is slow and fragile. setup-dotnet knows how to cache packages directly:

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

For caching to work, you need lock files. You enable them by putting in your projects:

<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

After the first restore, you will have a packages.lock.json next to each .csproj – you commit it to the repo. From now on, runs reuse already downloaded packages and builds become noticeably faster.


🔁 The central idea of the series: build once, deploy many

This is the principle that holds the whole series together, so it’s worth emphasizing:

You build the artifact once, then promote exactly the same artifact through dev → staging → production.

Why does this matter so much? Because if you rebuild the application separately for each environment, the binary that reaches production is no longer the one you tested in staging. A dependency version that changes in the meantime, a slightly different SDK on the runner, a build variable – and you have a subtly different artifact in prod.

The professional rule is simple: build once in CI, upload the artifact, and deploy in each environment means just "take that artifact and put it there." What differs between environments is the configuration (connection strings, feature flags), not the binary.


🔒 A word about security

Before we go further, three basic habits we adopt from the start:

  • Fix action versions (actions/checkout@v4), don’t use references that can change under you.

  • Give the pipeline only the permissions it needs, with the permissions block in the workflow.

  • No secrets in code and no passwords in YAML – in article 5 we see how to authenticate to Azure completely without passwords, through Managed Identity and OIDC.


🗺️ What’s next in the series

Now that we have the skeleton – build once, blocking tests, promotable artifact – we build on top of it:

  • Part 2 – Environments (dev/staging/prod) with approval gates: promote the same artifact through environments, with human approval before production.

  • Part 3 – Infrastructure as Code with Bicep: create Azure resources from code, not from the portal.

  • Part 4 – Deployment slots for zero-downtime: swap between staging and production without any downtime.

  • Part 5 – Secrets without passwords: authenticate to Azure through Managed Identity and OIDC, without client secrets.


Conclusion

A professional pipeline doesn’t mean more YAML, but better decisions: you build once, let tests block what needs to be blocked, produce a versioned artifact that you promote further, and prepare the ground for environments, approvals, and safe deploy.

In the next article, we take this artifact and pass it through dev, staging, and production – with a real approval gate before prod.