Deploying with CI/CD: Automating Test, Build and Deployment with GitHub Actions
We cover CI/CD: what continuous integration and continuous deployment mean, why an automated pipeline is needed, setting up a workflow with GitHub Actions, and automating test-build-deploy on every change.
In the previous article we containerized the application with Docker and put it into a portable package. But we still do these steps by hand: change the code, run the tests, build the image, move it to the server. Running this cycle by hand every time is both tiring and error-prone. You forget a step, you send the wrong version. Today we automate this cycle: with CI/CD, so that everything runs by itself every time you update the code.
What Is CI/CD?
Let's unpack the two abbreviations. CI means "Continuous Integration." Its idea is: automatically compiling and testing every code change. So when you break something, you find out instantly, not days later. CD means "Continuous Deployment" (or Delivery), that is, continuous deployment. This is the code that passes the tests being automatically prepared for deployment, or even directly released to production.
Together, the two work like an assembly line. In a factory, raw material enters from one end, goes through each station as it moves along the line (assembly, inspection, packaging), and finished product comes out the other end. CI/CD sets up this line for your code: the code enters from one end, is automatically tested, compiled, packaged, and comes out the other end ready for deployment. No human hand touches each station separately; the line flows by itself.
Why Automate?
Every manually done operation is open to being forgotten and to error. One day you forget to run the tests, and broken code goes to production. Another day you copy the wrong file. On top of that, these steps are boring, and people do boring jobs reluctantly, and therefore carelessly. Automation solves all these problems at once: the machine doesn't tire, doesn't forget, and applies the same steps with the same care every time.
Another important benefit is confidence. If you have a CI/CD pipeline, since every change automatically passes the tests, you're not afraid to touch the code. When you break something, the pipeline instantly turns red and warns you. In the twenty-fourth article we said that testing is a safety net; CI/CD is the mechanism that automatically stretches that safety net on every change.
GitHub Actions: The Tool of Automation
To set up this automation, you need a tool, and one of the most common is GitHub Actions. If your code is already on GitHub, you don't need an extra service; GitHub Actions runs directly inside the project. Its idea is simple: when certain events happen (for example, every time you push the code), it automatically runs the steps you defined.
You tell GitHub Actions what to do with a workflow file. This file stays in a special folder inside the project (.github/workflows) and is in YAML format. In it you define two basic things: when this automation will run and which steps it'll follow when it runs.
Writing the First Workflow
Let's start with a simple workflow. Our aim: automatically compile the project and run the tests every time the code is pushed. That is, set up the CI part. The file looks roughly like this:
name: CI
on:
push:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run tests
run: dotnet test --no-build
Let's read the file step by step. The on section at the top says when this workflow will run: every time code is pushed to the main branch. The jobs section below defines the work to be done. runs-on: ubuntu-latest specifies that this job will run on a clean Linux machine; a fresh, spotless environment is set up from scratch every time. steps are the steps to be followed in order: first check out the code, then set up .NET, restore dependencies, build, and run the tests.
What Happens When the Pipeline Runs?
The moment you add this file to your project and push it to GitHub, the magic begins. From now on, every time you push code to the main branch, GitHub Actions automatically kicks in: it sets up a clean machine, checks out your code, compiles it, and runs all your tests. It shows you the result too; if everything's fine you see a green check, if something broke you see a red warning.
The everyday equivalent of this is: you're working on the same project with a friend, they push a change but unknowingly break a test. Previously you'd only notice this on your own machine, maybe days later. Now the pipeline instantly turns red and everyone sees the break at that moment. This is an invaluable assurance in teamwork.
CD: Adding Deployment Too
Up to here we've set up the CI part, that is, automatic testing and compilation. The CD part is a step beyond: automatically preparing the code that passes the tests for deployment. The Docker we learned in the previous article comes right in here. After the tests pass, the pipeline can automatically build the application's Docker image and push it to an image registry; from there it can deploy it to the target server or cloud environment.
So a full CI/CD pipeline flows like this: you push the code, it's automatically compiled, the tests run, and if they all pass, the Docker image is built and deployed. You've only pushed the code; the rest happens by itself. The details of the deployment steps change depending on which server or which cloud provider it goes to; but the basic idea is always the same: the human just writes code, and the machine does everything repetitive.
A Small Experiment
Move your project to GitHub (if it isn't there yet). Then create the .github/workflows folder inside the project and add a workflow file like above. Push this file to GitHub and go to the project's "Actions" tab; see that your pipeline starts running automatically. Watch how each step (setup, build, test) is executed in order. If you like, deliberately break a test, push the code, and observe how the pipeline turns red and warns you. This experiment turns automation from an abstract concept into a concrete assurance that runs on every commit.
The next article is the last of the series. It'll touch on performance and optimization, bring together everything we've learned, and wrap up this long journey as a whole. Your API is now written, tested, packaged, and automatically deployed; in the last article we'll look at the ways to make it even better and where you can go from here.