Hey everyone! 👋
I’ve been neck-deep in configuring CI/CD for our team’s Python monorepo (multiple services, shared libraries, the whole shebang) and just wrapped up a pretty intense evaluation period. We tested GitHub Actions, CircleCI, and GitLab CI over the last few months. I kept a detailed log of all the hiccups, promises, and realities.
Here’s my honest breakdown:
**What we needed:**
- Fast, smart caching for Python dependencies and shared packages across services.
- Easy matrix testing across Python versions and OSes.
- Clean organization for dozens of workflows without config spaghetti.
- Reasonable costs at scale (we hit ~5k builds/month).
**The Vendor Pitch vs. Reality:**
* **GitHub Actions:** The "seamless" integration story. The monorepo-aware `paths:` filters are a lifesaver for triggering only changed services. However, the caching story for Python is… finicky. The `actions/cache` key strategy felt like a part-time job to get right, and we hit obscure failures where caches wouldn’t restore correctly across different job runners. Their macOS runner costs also ballooned faster than expected.
* **CircleCI:** Their "smart" monorepo logic is powerful on paper. We loved the idea of automatic change detection. In practice, the `paths:` filtering was less intuitive than GitHub’s, and their configuration (orb or not) felt like another layer to debug. The killer? Build times were great, but the pricing model after their changes made our finance team do a double-take. The vendor promised "simple, predictable pricing," but our invoice was anything but.
* **GitLab CI:** The most flexible by far. The ability to define reusable components and use `extends:` kept our `.gitlab-ci.yml` actually maintainable. The built-in Docker layer caching and package registry felt cohesive. The downside? The learning curve is steeper, and if you’re not all-in on GitLab, the context switching adds friction. Their SaaS runners can also get pricey, but bringing our own runners was surprisingly smooth.
**Would I renew?**
- **GitHub Actions:** Yes, but with gritted teeth. It’s "good enough" and the convenience is huge, but I’m praying for better native Python caching.
- **CircleCI:** No. The value just wasn’t there for us once the costs scaled. The trust in pricing stability is gone.
- **GitLab CI:** Yes, enthusiastically. For a complex monorepo, the control and power won out. We’re moving forward with it, using self-hosted runners for cost control.
Biggest pitfall across all? Assuming any tool’s "monorepo support" works out-of-the-box. Each required significant tuning and understanding of their specific caching and pipeline graph semantics.
What has been your experience? Did you hit different edge cases?
edge cases matter
I'm a backend lead at a 150-person fintech, and we run a Django monorepo in production with GitLab CI.
I'd break it down by what actually mattered for us:
1. **Monorepo workflow logic** - GitLab CI's `rules:changes` was way clearer for us than GitHub's `paths:` and Circle's orbs. One YAML file per service, with shared includes, kept things organized. We have about 30 services.
2. **Python caching performance** - We use GitLab's cache key with `$CI_COMMIT_REF_SLUG` and pip's `--cache-dir`. It's reliable. On GHA, cache misses added ~3 minutes per job for us. Circle's layer caching was fast but felt like a black box.
3. **Cost structure at scale** - At ~5k builds/month, GitLab's bundled minutes on their Premium tier ($29/user/mo) covered most of it. GitHub's macOS runners spiked our bill. Circle's per-minute compute cost felt less predictable month-to-month.
4. **Debugging experience** - GitLab's web UI for browsing job artifacts and logs is superior. Circle's UI is fast but cramped. GHA logs, when a runner fails mid-job, were sometimes truncated and harder to parse.
I'd pick GitLab CI for a Python monorepo. It's the least surprising once you get past the initial config hump. If you're already on GitHub for everything else, it's a harder call. To decide cleanly, tell us your current code hosting and if you have any Windows/macOS build requirements.
Still learning.
Your point about cache misses adding three minutes on GHA is oddly precise, which makes me trust it. That kind of granular pain is what you only get from living with it.
But I'm skeptical about GitLab's cost advantage being a universal truth. That $29/user/mo premium tier looks great until you need more concurrent runners for those 30 services. The bundled minutes get shredded during a busy sprint, and then you're on the hook for overage fees that feel just as unpredictable as Circle's compute cost. Their pricing page is a maze of conditional logic.
And while `rules:changes` is clearer for monorepos, I've hit race conditions on closely related commits that still triggered full pipelines. The "least surprising" part only applies if your team's change patterns are perfectly predictable, which mine never are.