Hello everyone! 👋 I've been living in the world of marketing automation and segmentation for so long, where comparing tools and their granular performance is just part of the daily routine. So when our engineering team decided to migrate our primary marketing platform monorepo from Jenkins to GitHub Actions, I just had to roll up my sleeves and dive into the details with them! It was a fantastic, methodical project, and I thought I'd share our step-by-step journey and the concrete benchmarks we captured.
First, let me set the scene. Our monorepo:
* Contains our main marketing site, a set of shared component libraries, and several internal campaign tools.
* Size: Roughly 85,000 lines of code across 12 distinct projects.
* Team: A core of 5 developers, with occasional contributions from 10+ marketers (who submit copy/asset updates via PR).
* Old Jenkins Pipeline: A single, complex `Jenkinsfile` that used declarative stages. It was becoming a bit of a "black box" and was painful to debug.
Our primary motivations for moving were:
* **Context Switching:** Having our code and our CI/CD in separate places created friction.
* **Configuration Simplicity:** Writing `.yml` files living right in the repo felt more intuitive than managing a separate Jenkins script.
* **Community Actions:** The ability to leverage pre-built actions for common tasks (like setting up Node.js or deploying to our cloud provider) was a huge draw.
Hereβs the high-level migration path we followed, with some key performance comparisons:
**Phase 1: Audit & Map**
We started by documenting every single step in our existing Jenkins pipeline. We broke it down into:
* Core jobs (build, test, lint)
* Conditional jobs (deploy to staging only on certain branches)
* Notifications (Slack, email alerts for failures)
**Phase 2: Recreate in Stages (The "Strangler Fig" Approach)**
Instead of a big-bang cutover, we recreated pipelines piece by piece in GitHub Actions, running them in parallel to compare outputs and timings. This was crucial for validation.
* **Benchmark: Initial Build & Test Suite**
* *Jenkins:* Average runtime of **14 minutes, 22 seconds**. This included spin-up time for a fresh agent.
* *GitHub Actions:* Average runtime of **11 minutes, 45 seconds** using the `ubuntu-latest` runner. The caching of dependencies (like `node_modules`) was significantly more straightforward to configure.
**Phase 3: Implement Monorepo-Specific Optimizations**
This was the most interesting part! We used path filters to trigger jobs only when relevant code changed.
```yaml
# Example for a shared library project
on:
push:
paths:
- 'shared-components/**'
- '.github/workflows/shared-components-ci.yml'
```
We also implemented a build matrix to test across multiple Node.js versions simultaneously, which actually *reduced* our total test cycle time compared to running them sequentially in Jenkins.
**Phase 4: Cutover & Final Benchmarks**
After two weeks of parallel runs, we switched the main branch to use GitHub Actions exclusively.
* **Key Metric: Lead Time for Changes (from commit to deploy)**
* *Jenkins Average:* **~28 minutes**
* *GitHub Actions Average:* **~19 minutes**
* The reduction came from eliminated queue times and faster artifact uploads/downloads.
* **Key Metric: Pipeline Configuration Maintainability**
* This is subjective but huge. Our engineers reported that making a tweak to the workflow now takes minutes instead of hours. The YAML configs are right next to the code they affect.
Has anyone else made a similar move for a monorepo? I'd be especially curious to hear about strategies for managing secrets or complex deployment gates in GitHub Actions compared to Jenkins. The side-by-side comparison of these two tools, from setup to execution, was such a rewarding deep dive
test everything twice
That moment when you described the Jenkinsfile as a "black box" hit home. We had a similar beast for our email campaign API, and debugging it felt like archaeology, digging through layers of old decisions. Your point about context switching is huge, too - having CI logic live right next to the code it builds just feels more natural.
I'm really curious to see how you handled the team aspect, especially with those marketer PRs. Did you find that moving to GitHub Actions made the CI process more transparent for your non-dev contributors? That's an angle I'm always thinking about. Looking forward to the next part
Happy testing!
Good question. The PR checks became a lot clearer for them. Instead of a generic "build failed" in Jenkins, they could see the exact step that failed in the GitHub UI, like a linting error in a content file. It cut down the back-and-forth.
Did you run into any specific friction when training non-engineers on the new workflow?
Exactly. That visibility for non-devs is the real win. The friction came from them having to learn a new interface, not the logic.
We got a few panicked "Why is my build red?" messages for the first two weeks. The issue was people looking at the wrong part of the GitHub PR UI. They'd see the overall red X but miss the expanded "Checks" tab where the actionable error lived. A five-minute screen recording showing where to click solved 90% of it.
My caveat: this only works if your actions are well-named. If your step is just "Run script", you've recreated the Jenkins black box. We forced descriptive step names like "Lint Content YAML" so the failure message was self-explanatory.
Absolutely nailed it with the descriptive step names. We used a similar rule and called it "the principle of least surprise" for the team. If someone sees "Lint Content YAML failed," they know not to ping an engineer about a database connection.
That five-minute recording idea is gold. We made a tiny internal wiki page with two screenshots: one pointing at the scary red X, and another with a big arrow to the "Checks" tab. It became our most-linked page for a month.
One extra thing that helped us was setting status checks as required in the branch protection rules. That way, the PR literally can't be merged until those specific, well-named checks pass. It trains the muscle memory of looking there.
Data doesn't lie, but dashboards sometimes do.
You're right that branch protection with required status checks creates a powerful forcing function. We found a related nuance: the order of those required checks matters more than you'd think.
In our monorepo, we configured it so the fast, deterministic checks (formatting, linting) ran and were required first. If those failed, the slower, more expensive integration tests wouldn't even trigger. This prevented wasteful resource consumption on a PR that would fail on a simple syntax error anyway. It also trained the team to fix the obvious, cheap problems first because the build would stop there immediately.
The downside is that it requires careful dependency management within your workflow file to create that logical gating, but it pays off in reduced compute costs and clearer feedback loops.
That's a really smart point about ordering the checks, thank you for sharing it. I hadn't considered the compute cost angle at all, but it makes perfect sense. It's like setting up a polite but firm gatekeeper that saves everyone time and money.
In our onboarding workflows, we try to apply a similar logic by having validation checks run before any system provisioning steps. This does require careful orchestration, like you said, but it prevents creating a bunch of empty user accounts if the initial data is invalid.
Did you run into any issues with developers who felt the gating was too restrictive, or did the benefit of faster, cheaper feedback make the case for itself?
Great point about "polite but firm gatekeeper." We frame it to the team as helping them fail fast and cheap, which most devs appreciate. Any initial grumbling vanished when they got their lint results in 30 seconds instead of waiting 10 minutes for a full suite to bomb.
That orchestration you mentioned is key. We use the `needs` keyword in our workflow YAML to create explicit dependencies, which makes the gating logic very clear in the code itself. It also prevents that feeling of arbitrary restriction because the rules are right there, in the repo, next to the code they affect.
dk
Absolutely, the `needs` keyword is the linchpin for that logical gating. A small caveat we discovered, however, is that this explicit dependency graph can sometimes lead to *too much* serialization, slowing down the total workflow for changes that touch independent services.
We mitigated this by grouping parallelizable jobs, like linting for different project directories, into a single parent job that uses a matrix strategy. That job then has a single `id` for downstream jobs to `need`, keeping the graph clean while still allowing concurrency where safe. It's a balance between a clear, enforced sequence and avoiding an artificially slow pipeline.
Your data is only as good as your pipeline.
That's a great summary of the foundational reasons for making the move. I'd add that the separation between code and CI configuration often leads to knowledge silos. When everything's in the same repo, onboarding a new engineer means they can understand the full delivery pipeline alongside the business logic, which is a huge win for team resilience. Your point about debugging a "black box" Jenkinsfile really resonates there.
Oh man, I feel that "black box" Jenkinsfile pain so much. Your setup sounds a lot like ours was before we jumped ship. That context switching is a silent productivity killer, isn't it? You'd have a tab for the repo, a tab for Jenkins, and you're constantly flipping to see why a build failed, trying to mentally map the error back to the code.
Your point about configuration simplicity is the real clincher for me. Having the workflow YAML right there next to the code it builds creates such a beautiful feedback loop. You can tweak a build step and see the result in the PR within minutes, all without leaving GitHub. It feels like you're actually fixing the problem, not just sending a change request into a void.
I'm really curious, since you mentioned the team includes marketers submitting PRs for copy: did you find that having the CI config in-repo made those non-engineers feel more or less intimidated by the whole process? Like, could they see the checks they needed to pass, or was it just more "scary code" in the file list?
hugo