Jenkins served well. But 22 repos with 22 snowflake pipelines? That's not CI/CD. That's chaos with a green icon.
Let me paint the picture. Twenty-two repositories. Each with its own Jenkins pipeline. Each written by a different developer in a different year with a different understanding of what "deployment" means. Some had tests. Some had... aspirations of tests. One had a comment that said "TODO: add tests" dated 2021.
Every pipeline was a unique snowflake. Beautiful in isolation. A nightmare at scale.
When a deployment pattern needed to change — say, adding a health check after deploy — someone had to touch 22 files. In 22 repos. With 22 slightly different syntaxes. And hope that the developer who wrote pipeline #7 three years ago left enough comments to understand what's happening.
They didn't.
Why Jenkins Had to Go
I'm not a Jenkins hater. Jenkins is a workhorse. It's been running builds since before half the industry knew what CI meant. But Jenkins in 2025 for a team migrating to GitLab is like driving a horse-drawn carriage on a highway. Technically possible. Practically painful.
The real problems:
- No native Git integration. Jenkins treats code as input. GitLab treats code as context. Merge request pipelines, branch-based deployments, environment tracking — all native in GitLab, all plugins in Jenkins.
- Plugin hell. Every Jenkins capability requires a plugin. Every plugin has its own update cycle, security advisories, and compatibility matrix. I've seen Jenkins instances with 80+ plugins where nobody knew which ones were actually needed.
- Snowflake pipelines. Without a shared template system, every team builds their own pipeline from scratch. Same patterns, different implementations. Multiply by 22 repos.
The Template Architecture
I didn't write 22 GitLab CI pipelines. I wrote one.
One enterprise-grade template that every repo includes. One source of truth for how builds, tests, and deployments work. One place to fix a bug, and all 22 repos get the fix instantly.
The template handles:
Build stage: .NET, Node, Java — auto-detected by project structure. Build once, artifact everywhere. The same binary that passes tests in DEV is the one that goes to production. No "works on my machine."
Test stage: Unit tests with mandatory coverage enforcement. Not "we have tests." More like "if coverage drops below threshold, your pipeline dies." Because optional tests are no tests.
Security gates: Dependency scanning, secret detection, SAST. Baked into the template, not opt-in. You don't get to choose whether your code is scanned. It is.
Deployment stage: Multi-environment progression — DEV, UAT, PROD — with mandatory validation at each gate. You can't skip to production. You physically can't. The pipeline enforces DEV → UAT → PROD. Always.
Zero-downtime slots: Production deployments use Azure App Service deployment slots. Deploy to staging slot, validate, swap. If something breaks, swap back. No downtime. No prayers.
The Migration Pattern
Migrating 22 repos isn't "copy-paste the Jenkinsfile into .gitlab-ci.yml." It's a systematic process:
-
Audit each repo's Jenkins pipeline. Understand what it actually does versus what someone thinks it does. These are often different things.
-
Map Jenkins stages to GitLab stages. Most translate directly. Some reveal that the Jenkins pipeline was doing things nobody knew about. Some reveal it was doing nothing useful at all.
-
Replace repo-specific logic with template includes. The repo's
.gitlab-ci.ymlbecomes 15 lines: include the template, set your variables, done. -
Validate by running both pipelines in parallel for a sprint. Compare outputs. Fix discrepancies. Build confidence.
-
Cut over. Disable Jenkins. No gradual sunset. Clean break.
The result: 22 repos, each with a .gitlab-ci.yml that's shorter than this paragraph. All pointing to one shared template. All deploying the same way. All tested the same way. All secured the same way.
The Coverage Argument
"We'll add tests later."
No. You won't. "Later" in software development means "never." I've seen it enough times to know.
The template enforces test coverage from day one. Not 100% — that's a vanity metric. But a meaningful threshold that catches regressions. If your merge request drops coverage, it doesn't merge. End of discussion.
This is the part where developers push back. "But my service is different." "But we're in a hurry." "But the tests are flaky."
Your service follows the same deployment pattern as 21 others. You're always in a hurry. And flaky tests are a bug, not an excuse.
Shared Scripts via Package Registry
Here's a trick that saved weeks: PowerShell deployment scripts stored in GitLab's Package Registry.
Instead of copy-pasting scripts into every repo, the template fetches the latest version from the registry at pipeline runtime. Update the script once, publish, and every pipeline picks it up on the next run.
Version pinning is available for teams that need stability. But the default is always "latest" — because shared scripts should evolve with the platform, not stagnate in someone's fork.
What Changed
Before: 22 snowflakes. No standard. No enforcement. "Deployment" meant something different in every repo.
After: One template. Mandatory testing. Mandatory security scanning. Mandatory environment progression. Zero-downtime production deployments.
The teams resisted at first. Change is uncomfortable. But after the first production deploy went through without a single manual step — build, test, scan, deploy DEV, validate, deploy UAT, validate, manual approval, deploy PROD, validate — all in one pipeline run... the resistance evaporated.
Because nobody misses Jenkins when the new system just works.
22 repos. One pipeline. Zero snowflakes. 100% coverage enforcement.
That's not a migration. That's an upgrade.