AI needs context. Your database architecture matters, and split data won't cut it.
TigerData puts everything in one PostgreSQL + TimescaleDB instance for you, and lets the schema do the work. Close the AI agent readiness gap by querying through MCP.
AI coding agents are writing code faster than any review process was designed to handle. CI catches the problem an hour later: the wrong library, the wrong layer, a pattern your team dropped two years ago. By then the agent has moved on, and fixing it means reconstructing what it was thinking when it wrote the code.
Sonar Vortex moves that check inside the agent's loop. Before the agent writes anything, it feeds it your actual architecture β class hierarchies, call flows, approved dependencies, your team's coding standards. Then it verifies each change as the agent produces it, running the same analysis SonarQube runs in CI, in seconds.
In Sonar's own testing with a leading coding agent, that cut issues produced by 92% and token costs by up to 36%. It runs through the SonarQube CLI or a local MCP server, so it fits the agent setup you already have.
Did the build pass? Did the tests run? Is the Docker image ready to ship?
If you are checking these by hand, you are doing work a machine should do for you.
A good CI/CD pipeline builds, tests, and ships every change the same way, every time - without you babysitting it.
Over the years, I have set up these pipelines for many .NET projects, and GitHub Actions has become my default.
It lives right next to your code, it is free for most projects, and it can take a single commit all the way from a pull request to a published Docker image.
GitHub Actions is a built-in automation platform inside every GitHub repository.
You describe what should happen when something occurs in your repository - a push, a pull request, a new tag, a schedule or manual - and GitHub runs it for you on a fresh virtual machine.
You need to know these terms to be able to read any workflow:
Workflow: an automated process defined in a YAML file under .github/workflows. A repository can have many.
Event: what triggers a workflow - a push, a pull_request, a release, or a schedule.
Job: a group of steps that runs on one machine. Jobs run in parallel by default, or in order when one needs another.
Step: a single task - either a shell command (run) or a reusable action (uses).
Runner: the machine that executes a job. GitHub hosts Linux, Windows, and macOS runners, and you can host your own.
Any workflow in GitHub Actions is a series of steps that run on a virtual machine.
With these, you can automate almost anything related to your code: run tests on every pull request, publish a NuGet package with a new release, build and push a Docker image, run a nightly security scan, or post a message to Slack.
Because a workflow is just a file in your repository, it is versioned with your code and reviewed in pull requests like everything else.
The pipeline evolves together with the application it builds.
Every workflow is a single YAML file.
Here is the smallest one that does real work - it runs on each push to main and prints a line on an Ubuntu runner:
yaml
1name: HELLO
23on:4push:5branches:["main"]67jobs:8hello:9runs-on: ubuntu-latest
10steps:11-run: echo "GitHub Actions is running"
Save this as .github/workflows/hello.yml, push it, and open the Actions tab in your repository to watch it run.
Let's explore an example of a ShippingService Web API that tracks shipments, backed by PostgreSQL.
The solution has a unit test project and an integration test project, and the first job every pipeline needs is to restore, build, and test on every change.
actions/checkout@v4 clones your repository onto the runner so the rest of the steps can see your code.
actions/setup-dotnet@v4 installs the .NET 10 SDK.
dotnet restore, then dotnet build --no-restore, then dotnet test --no-build - each step reuses the previous one's output, so you compile once and test exactly what you built.
--configuration Release tests the same build you will ship, not a debug build.
This runs on every push to main and on every pull request, so a broken test blocks the merge before it reaches your main branch.
Each job shows up as a status check on the pull request, with a green tick or a red cross and a full log you can open.
The dotnet test step decides the result: if any test fails, the step returns a non-zero exit code, the job turns red, and the merge is blocked.
Because the container lives inside the test run, the workflow above already works.
GitHub's Ubuntu runners have Docker pre-installed, and Testcontainers uses it automatically - no services: block, no connection strings in YAML, no cleanup step.
The same tests that pass on your machine pass in the pipeline.
Note: if you would rather manage the database in the workflow, GitHub Actions supports services: containers - a postgres service that GitHub starts before your job and tears down after. Testcontainers keeps that detail inside the test project, which is why I prefer it.
A library does nothing for anyone until it is published.
If your repository includes a reusable library - in our case, Shipping.Contracts, the shared models other services depend on - you can publish it to NuGet automatically, but only when the tests are green.
Add a second job that needs the first. A job with needs waits for the named job to succeed, so the package never ships on a red build:
needs: build-and-test chains the jobs - publishing runs only after the build-and-test job passes.
if: startsWith(github.ref, 'refs/tags/v') limits publishing to version tags like v1.2.0, so a normal push to main never publishes a package. You tag a release, and the package goes out.
dotnet pack produces the .nupkg file from your library project.
dotnet nuget push uploads it to nuget.org, authenticating with a secret API key. --skip-duplicate keeps a re-run safe if that version already exists.
Tag-based publishing fits semantic versioning cleanly: you push a tag like v1.2.0, the workflow packs that exact commit, and the tag's version becomes the NuGet version. Day-to-day commits keep flowing to main without releasing anything.
You store the NUGET_API_KEY value as a repository secret, never in the YAML - the Setting Up Secrets section below shows how.
Note: to publish to GitHub Packages instead of nuget.org, point --source at https://nuget.pkg.github.com/<owner>/index.json and authenticate with the built-in GITHUB_TOKEN.
A multi-stage Dockerfile builds the app in one stage and copies only the result into a small runtime image.
You can add a test stage in the middle, so the tests run as part of the build - if a test fails, the image is never produced:
dockerfile
1# Build stage2FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build3WORKDIR /src4COPY . .5RUN dotnet restore6RUN dotnet build --no-restore --configuration Release78# Test stage - the build fails here if any test fails9FROM build AS test10RUN dotnet test --no-build --configuration Release1112# Publish stage13FROM build AS publish14RUN dotnet publish src/ShippingService/ShippingService.csproj \15 --no-build --configuration Release -o /app1617# Runtime stage - small final image, no SDK18FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final19WORKDIR /app20COPY--from=publish /app .21ENTRYPOINT ["dotnet", "ShippingService.dll"]
Each FROM starts a new stage:
build restores and compiles the solution on the full .NET SDK image.
test runs dotnet test on top of the build. If a test fails, this layer fails and the whole docker build stops.
publish produces the trimmed, ready-to-run output.
final copies that output onto the lightweight ASP.NET runtime image - no SDK, no source code, a much smaller image to deploy.
Note: Keeping the SDK out of the final image is not only about size. The runtime image ships without compilers or build tools, which means a faster pull on every deployment and a smaller attack surface in production.
Now build that image in the workflow.
This job also waits for the fast unit-test job first, so you fail cheaply before spending time on a Docker build:
Repository secrets are available to every workflow in the repo. Environment secrets belong to a named environment like production and can require manual approval before a job uses them.
GitHub masks secret values in logs. If a secret ever lands in output, it shows as ***.
You cannot read a secret back in the UI after saving it. To change it, you overwrite it.
Every workflow automatically gets a GITHUB_TOKEN secret - a short-lived token scoped to the repository. You will use it in the next section to push to the container registry, with no setup at all.
Treat secrets as the only place credentials belong. A value in ${{ secrets.X }} is safe; the same value typed into YAML is a leak waiting to happen.
Also, I recommend you turn on multifactor authentication so that whenever you need to change your secret, you'll be prompted to verify your second factor, such as a Passkey or your GitHub mobile app.
A built image does nothing until it lives somewhere your servers can pull it.
That place is a container registry.
This example pushes to the GitHub Container Registry (ghcr.io), because it needs no extra secret - the automatic GITHUB_TOKEN from the previous section is enough.
Here is the complete Docker job.
It logs in to the registry, then builds and pushes in one step with the official docker/build-push-action:
permissions: packages: write lets GITHUB_TOKEN push to the registry. Without it, the push is denied.
docker/login-action signs in to ghcr.io using the workflow's own identity (github.actor) and the built-in token - no secret to manage.
docker/build-push-action runs the same multi-stage Dockerfile, so the tests still run during the build, then pushes the result.
Two tags: latest for the most recent image, and ${{ github.sha }} so you can always trace an image back to the commit that produced it.
After this runs, your image is available at ghcr.io/<owner>/<repo>, ready to pull. By default, the image is private to your repository - other workflows can pull it with the same GITHUB_TOKEN, and you can make the package public from its page in your GitHub profile.
From here to a running container is one more pull on your host. On a server with Docker, deployment is:
bash
1docker pull ghcr.io/<owner>/<repo>:latest
2docker compose up -d
You can run that over SSH from a final workflow step, hand the image to a platform like Azure Web App for Containers, or let an orchestrator such as Kubernetes pull the new tag.
The image is the same trusted artifact either way.
GitHub Actions takes a commit from a pull request all the way to a published Docker image, without you touching a thing.
Let's recap the key takeaways:
Start with build and test. A workflow that runs restore, build, and test on every push and pull request is the foundation - it keeps broken code from merging.
Use Testcontainers for integration tests. They spin up a real database from your test code, so the same tests pass locally and in the pipeline with no workflow configuration.
Gate releases on green tests. Chain jobs with needs: and trigger NuGet publishing on a version tag, so a package only ships when the tests pass.
Run tests inside the Docker build. A multi-stage Dockerfile with a test stage guarantees you never produce an untested image.
Keep secrets out of YAML. Store API keys and passwords as GitHub Secrets, reference them with ${{ secrets.NAME }}, and lean on the automatic GITHUB_TOKEN where you can.
Push to a registry. Publishing the image to ghcr.io gives every environment one trusted artifact to pull.
You do not need a complex setup to get value from CI/CD.
Start with a single build-and-test workflow, then add publishing, Docker, and deployment one step at a time as your project grows.
Hope you find this newsletter useful. See you next time.
Whenever you're ready, here's how I can help you:
The .NET Senior Playbook is built to:
Fast-track you from junior or mid-level to senior
Keep you growing as a senior
Help you beat any .NET interview
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.
Not sure where you stand? Take the free .NET Interview Run:
Find out your real level β Junior to Senior+
A realistic mock .NET interview β across 13 areas of C#, .NET, ASP.NET Core and System Design
No credit card required. When you finish, you get a personalized report: your level, your strongest and weakest areas, and where to focus next β the perfect way to benchmark yourself before diving into the Playbook.