A good pipeline is invisible: engineers push, tests run, a staging environment updates in minutes and a production release is a button press with a rollback path. A bad pipeline is a Friday-night ritual. Here is the blueprint we install on every project — on Azure DevOps or GitHub Actions, whichever your organisation uses.
The five stages
- Build — restore, compile, produce a versioned artifact once. Never rebuild for a later stage.
- Test — unit and integration tests with coverage; fail fast.
- Scan — static analysis (SonarQube), dependency vulnerability checks, secret scanning.
- Deploy to staging — automatic on
main, with smoke tests and database migrations. - Promote to production — manual approval, blue/green or slot swap, automatic rollback on failed health checks.
A .NET API on GitHub Actions
# .github/workflows/api.yml — build, test, deploy a .NET API to Azure
name: api
on: { push: { branches: [main] } }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '9.0.x' }
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release --collect:"XPlat Code Coverage"
- run: dotnet publish src/Api -c Release -o out
- uses: azure/webapps-deploy@v3
with:
app-name: ishtar-api-prod
slot-name: staging
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: outDeploying to a staging slot and swapping is the key detail: the new version warms up, health checks pass, and the swap is instant and reversible.
A Flutter app on Azure DevOps
Mobile pipelines add signing and store delivery. We keep secrets (keystores, certificates, API keys) in the pipeline's secure files and variable groups — never in the repository — and inject environment configuration with --dart-define.
# azure-pipelines.yml — Flutter app build stage
stages:
- stage: Build
jobs:
- job: Android
pool: { vmImage: 'ubuntu-latest' }
steps:
- task: FlutterInstall@0
- script: flutter pub get && flutter test
- script: flutter build appbundle --release --dart-define=ENV=prod
- publish: build/app/outputs/bundle/release/app-release.aab
artifact: androidA second stage uploads the bundle to Google Play's internal track and the IPA to TestFlight, so testers receive every merged change automatically.
Infrastructure as code
Environments are defined in Terraform (or Bicep on Azure) and applied by the same pipelines. A new environment — a demo for a prospect, a region for a new market — is a pull request, not a week of clicking.
Practices that make the difference
- Trunk-based development with short-lived branches and required reviews.
- Semantic versioning stamped into the artifact and shown in the app's About screen.
- Database migrations in the pipeline, run before the app switches, backwards-compatible for one release.
- Feature flags so deployment and release are separate decisions.
- Observability gates — a deploy is not "done" until error rates and latency are healthy for ten minutes.
The result
Teams we set up this way ship to production several times a week with fewer incidents than teams shipping monthly. The pipeline is not overhead — it is the fastest feature you will ever build.