My first production deployment at Blue Yonder was entirely manual. Copy files to the server. Update the config by hand. Restart the service. Call the team to verify it was live. The whole process took 45 minutes, produced a slightly different result every time depending on who ran it, and only two people on the team knew all the steps.
Then we moved to automated pipelines. The same deployment became: push code to Git, wait 4 minutes, done. Same result every single time. Anyone on the team could trigger it. Full audit log of every deployment. Rollback in one command.
CI/CD is not a nice-to-have for serious software delivery. It is the foundation. This post covers everything — YAML syntax, GitHub Actions, Azure DevOps, deployment strategies, merge conflicts, and the security practices that keep pipelines safe.
CI vs CD — The Actual Difference
Most people use CI/CD as one term without knowing where one ends and the other begins.
Continuous Integration is about merging code frequently and automatically verifying it. Every time a developer pushes code, the CI system builds it and runs tests. The goal is to catch problems immediately — within minutes of the bad code being merged, not days later when someone manually tests the feature.
Continuous Deployment takes the verified build from CI and automatically deploys it to one or more environments. Continuous Delivery deploys automatically to staging but requires a human to approve the production deployment. Continuous Deployment goes all the way to production automatically with no human approval.
A complete pipeline looks like this:
Code pushed -> Build -> Unit tests -> Integration tests
-> Deploy to dev -> Deploy to staging -> Manual approval
-> Deploy to production.
TechStack Blog uses a simplified version of this — every push to main builds the C# API, publishes it, and deploys to Azure App Service automatically. No staging environment for a solo blog project, but the same principles apply.
YAML — The Language of Pipelines
YAML (Yet Another Markup Language) is the format used by GitHub Actions, Azure DevOps, and almost every modern CI/CD tool. It uses indentation to show structure and is readable as plain text.
The critical rule: indentation must be consistent and use spaces, never tabs. One wrong indent and the pipeline fails with a cryptic error.
The basic structure of any pipeline has three parts.
The trigger defines when the pipeline runs — on push, on pull request, on a schedule, or manually.
The jobs define what machines do the work and in what order.
The steps define the individual commands each machine runs.
- Lists use a dash at the start of each item.
- Objects use key-value pairs with a colon.
- Nested structure uses consistent indentation.
- Secrets and variables are referenced with double curly braces.
Understanding these four rules lets you read any YAML pipeline file regardless of which CI/CD platform wrote it.
GitHub Actions
GitHub Actions is CI/CD built directly into GitHub. Every workflow is a YAML file stored in the .github/workflows/ folder of your repository. When the trigger condition is met — a push, a PR, a schedule — GitHub spins up a fresh virtual machine, runs your steps, and tears the machine down when done.
The TechStack Blog API deployment workflow does eight things in sequence.
- It checks out the repository code.
- It installs the .NET 8 SDK.
- It restores NuGet packages.
- It builds the C# project in Release mode.
- It runs tests.
- It publishes the output to a folder.
- It zips that folder.
- It logs into Azure using service principal credentials stored as GitHub Secrets and deploys the zip to Azure App Service.
The entire process takes 3-4 minutes. Every push to main triggers it automatically.
The most important concept in GitHub Actions is job dependencies. The deploy job has "needs: build" which means it only runs if the build job succeeds. If the build fails the deployment never happens. This prevents
broken code from reaching Azure.
Secrets in GitHub Actions are stored encrypted and never appear in logs. They are referenced with the ${{ secrets.SECRET_NAME }} syntax. The Azure service principal credentials — client ID, tenant ID, and subscription ID — are stored as secrets so they never appear in the YAML file that is publicly visible in the repository.
Azure DevOps Pipelines
Azure DevOps is Microsoft's enterprise CI/CD platform. It has more features than GitHub Actions and is the standard in large organizations. At Blue Yonder, Azure DevOps managed our integration platform deployments
across multiple environments with formal approval workflows.
The key difference from GitHub Actions is the concept of stages with environments. You define a Production environment in Azure DevOps and configure it to require approval from specific people before any deployment can proceed. When the pipeline reaches the production stage, it pauses and sends an email to the approvers. Only after someone clicks Approve does the deployment continue.
This is the approval gate pattern — critical for any system where a bad deployment has real consequences. GitHub Actions has similar functionality through Environment protection rules, but Azure DevOps makes it more prominent and easier to configure for teams.
Azure DevOps also has variable groups — shared sets of variables that multiple pipelines can reference. Instead of duplicating the same connection string in ten different pipeline files, you define it once in
a variable group and all pipelines inherit it. When the value changes, you update it in one place.
Manual vs Automated Deployment
Automated deployment is the goal but manual deployment has its place. Know when to use each.
Automate everything that runs repeatedly and must produce identical results every time. Application code, configuration changes, dependency updates — these all belong in automated pipelines.
Keep manual control over things that require judgment. Database schema migrations that could lose data. First-time infrastructure provisioning with Terraform or Bicep. Emergency hotfixes that are faster to deploy manually than waiting for a pipeline. Deployments that require physically touching hardware.
The hybrid approach that works in practice: code deployments are fully automated, database migrations are automated with a manual approval step, infrastructure changes are manual with documentation, and production releases require a pipeline approval gate even though the deployment itself is automated.
Deployment Strategies
Blue-Green deployment runs two identical environments. Traffic goes to the blue environment (live). You deploy to the green environment (idle) and test it. When ready, you switch all traffic to green in seconds. Blue becomes the instant rollback target. Azure App Service deployment slots implement this pattern directly — swap slots to go live, swap back to roll back.
Canary deployment sends a small percentage of traffic to the new version first. Start at 5%, monitor for errors, increase to 25%, then 50%, then 100%. If errors appear at any stage, roll back the canary percentage.
Real users test the new version but only a small fraction of them are exposed to any problems.
Rolling deployment updates one server at a time while the others keep serving traffic. No downtime but slower to roll back — you have to wait for the rolling update to complete in reverse.
For a single-server deployment like TechStack Blog, the simplest strategy is stop, replace, start. Acceptable for a blog. Not acceptable for a system that needs zero downtime.
Merge Conflicts
A merge conflict happens when two developers change the same line of the same file and Git cannot automatically decide which change to keep. This is the moment most new developers dread. Once you understand it, conflicts
are completely manageable.
The scenario: main branch has a function returning "Hello". Developer A changes it to "Hello World" and merges first. Developer B changes the same line to "Hi there" and tries to merge. Git sees both changed the same line and does not know which version is correct. Git marks the file and asks you to decide.
What Git puts in the file:
<<<<<<< HEAD
your version of the code here
=======
their version of the code here
>>>>>>> their-branch-name
Everything between the less-than signs and the equals signs is your version. Everything between the equals signs and the greater-than signs is their version.
To resolve: delete both markers, keep whichever code is correct (or combine both), save the file, run git add on the resolved file, then git commit to complete the merge.
VS Code shows conflict markers with clickable buttons — Accept Current Change, Accept Incoming Change, Accept Both Changes. Click the appropriate button, VS Code removes the markers automatically, save and commit.
Preventing Merge Conflicts
Pull before you push every single time. This one habit eliminates the majority of conflicts by keeping your local branch close to the current state of main.
Commit small and commit often. Large commits that change many files across many features create maximum overlap with other developers' work. Small focused commits targeting specific files create minimum overlap.
Keep feature branches short-lived. Branches that live for weeks diverge significantly from main and produce massive conflicts when merged. Aim for branches that live 1-3 days and get merged back through a pull request.
Communicate about shared files. When you know you are making significant changes to a file that others also work in, mention it to the team. Simple coordination prevents most serious conflicts.
Branch Strategies
GitFlow uses long-lived branches for different purposes.
- A main branch contains only production-ready code.
- A develop branch is the integration point where features are merged before going to production.
- Feature branches come off develop and merge back into develop.
- Release branches prepare for a production deployment.
- Hotfix branches patch production issues directly.
This maps cleanly to CI/CD: develop branch triggers deployment to the dev environment, release branch triggers staging deployment, main branch triggers production deployment with approval.
Trunk-Based Development keeps almost everything in one branch — main (or trunk). Feature branches exist but are short-lived, typically 1-3 days maximum. Feature flags control what users see rather than long-lived branches. Every merge to main can safely deploy to production. This is the approach modern engineering teams use when they want to deploy multiple times per day.
TechStack Blog uses simplified trunk-based: one main branch, everything deploys automatically. Works perfectly for a solo developer project.
Pipeline Security
Never put actual values for secrets in YAML files. A YAML file in a public GitHub repository is visible to everyone. Any secret in that file is compromised the moment you commit it. Use ${{ secrets.NAME }} and
store the actual value in GitHub Secrets or Azure DevOps variable groups.
Pin your action versions. Using actions/checkout@main means your pipeline uses whatever version is current at the time it runs. That version can change without warning and silently break your pipeline or introduce malicious code. Use actions/checkout@v4 for a specific
stable version.
Give your service principal the minimum permissions it needs to deploy. If the pipeline only needs to deploy to one App Service, its Azure role should only allow that — not manage billing, not delete resources, not
create new services.
Use environment protection rules for production. In GitHub Settings, create a Production environment and require one or more specific people to approve before any deployment can proceed.
Key Lessons From Production
Set up CI/CD before writing business logic. Retrofitting a pipeline into an existing project is significantly harder than building it alongside the first commit. The TechStack Blog pipeline was configured on day one.
Keep pipelines fast. If building and deploying takes more than 10 minutes, developers stop waiting for results and start pushing anyway. Cache dependencies, run tests in parallel, and skip unnecessary steps.
Test your rollback before you need it. A rollback plan you have never practiced is not a plan. Every team should do a rollback drill in staging at least once per quarter.
Never let a broken pipeline stay broken. A broken pipeline is ignored. An ignored pipeline means changes go unverified. Fix pipeline failures immediately — treat them with the same urgency as a production outage.
Summary
CI/CD automates the path from code to running software. YAML defines the pipeline steps in a readable, version- controlled format. GitHub Actions brings CI/CD directly into your GitHub workflow with zero additional tooling. Azure DevOps adds enterprise approval gates and environment management for organizations that need them. Merge conflicts are a normal part of collaborative development — understand the markers, resolve deliberately, prevent through good habits. Together these practices make deployment reliable, repeatable, and safe.
The TechStack Blog goes from git push to live in 4 minutes automatically, every time, with no manual steps. That is what CI/CD is for.
Originally published at TechStack Blog:
https://www.techstackblog.com
Follow for weekly posts on Azure, C#, DevOps, and cloud engineering.
Top comments (0)