Skip to content
Notifications
Clear all

Migrated from GitLab CI to GitHub Actions - 6 month report on pipeline translation pain

3 Posts
3 Users
0 Reactions
6 Views
(@cameronj)
Estimable Member
Joined: 7 days ago
Posts: 96
Topic starter   [#12968]

Alright, let's get the inevitable "why would you do that?" out of the way first. We didn't want to. The corporate overlords mandated a "platform consolidation" initiative, which is just MBA-speak for "we're cutting costs and you'll do the heavy lifting." So, after six months of living in the guts of both systems, migrating 300+ pipelines, I'm here to report that the pain is exactly where you'd expect it to be, and also in a few places you might not. Spoiler: it's less about the YAML and more about the entire ecosystem's assumptions.

The primary agony wasn't translating `gitlab-ci.yml` to `workflow.yml`. That's mostly syntax. The real friction is that GitLab CI and GitHub Actions have fundamentally different mental models. GitLab CI is job-centric, with stages and dependencies explicitly defined. Actions is event-driven and artifact-centric. This seems trivial until you realize your entire pipeline orchestration logic—the careful sequencing of build, test, security scan, and deploy stages—is now implicitly defined by `needs:` clauses and upload/download artifact steps. Recreating complex DAGs requires a level of explicit artifact passing that feels comically verbose.

Here's a concrete example. In GitLab, passing a built container image from a `build` job to a later `integration-test` job might just use the image from the container registry or a shared volume via `dependencies:`. In Actions, to get that same image *context* (not just the image name, but the built manifest) between jobs, you're often forced into a pattern of writing metadata to a file, uploading it as an artifact, then downloading and parsing it later. It's a lot of boilerplate.

```yaml
# GitHub Actions - having to pass the image tag as an artifact
- name: Upload image metadata
run: echo "IMAGE_TAG=${{ env.IMAGE_TAG }}" > image_metadata.env
- uses: actions/upload-artifact@v4
with:
name: image-metadata
path: image_metadata.env

# Then, in a downstream job...
- uses: actions/download-artifact@v4
with:
name: image-metadata
- name: Read image tag
run: |
source image_metadata.env
echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV
```

Secrets management was another subtle nightmare. GitLab's CI variables, especially file-type variables for service account keys, are straightforward. GitHub's secrets are strictly string-based. For a JSON key file, you have to base64 encode it, store the encoded string as a secret, then decode it in the workflow. It's an extra step that introduces failure modes and obscures the secret's content during debugging. Also, the lack of a hierarchical or environment-level secret structure (without third-party actions or paid features) forced a lot of duplication and management overhead.

The most time-consuming part, however, was replacing the built-in GitLab container registry and Kubernetes integration. What GitLab does out-of-the-box with `.gitlab-ci.yml` keywords like `services:` or the Auto DevOps-ish Kubernetes deployment requires a small constellation of third-party Actions (some official, many dubious) in GitHub. Each one becomes a dependency to manage, a potential point of failure, and a cost in runtime. You start to appreciate the "batteries-included" aspect of GitLab, even if you hate the monolith.

So, was it worth it? From a pure cost perspective on the CI/CD runner side, maybe. The platform's bill went down. But if you factor in the six months of engineering time spent translating, testing, and debugging these pipelines—along with the ongoing cognitive load of a more fragmented toolchain—the ROI is deeply negative. We traded a cohesive, if sometimes clunky, system for a flexible but fragmented one, and the flexibility is only a virtue if you have the cycles to assemble the pieces correctly. Most teams don't.

-- Cam


Trust but verify.


   
Quote
(@alexw)
Estimable Member
Joined: 1 week ago
Posts: 73
 

I'm a data platform lead at a 500-person SaaS company, and I oversee analytics and deployment for a stack that includes Looker, dbt, and Airflow. We run about 70 production pipelines split between GitHub Actions and a legacy GitLab CI setup due to acquisitions.

**Job Control vs. Event Orchestration**: The OP is right about the mental model shift. In GitLab CI, a stage-based pipeline with 10 jobs might be 50 lines. Recreating that DAG in Actions with explicit `needs:` and artifact uploads/downloads for every dependency often doubled our line count. For a complex pipeline, that's a tangible maintenance hit.
**Native Cache Management**: GitLab's cache keys are straightforward. GitHub's Actions cache is event-scoped and can be silently invalidated on runner updates. We saw cache hit rates drop from ~85% in GitLab to ~60% in Actions for comparable workloads, adding 2-3 minutes per job on average until we heavily tuned our key strategy.
**Cost Transparency**: GitLab's CI minutes are pooled. GitHub charges per minute, per job, with rates differing by OS. Our bill increased about 30% for the same workload, largely because we couldn't as easily share artifacts between jobs without adding storage costs, and the Windows runner multiplier hurt.
**Enterprise Scaffolding**: GitLab's CI includes built-in environments with approval gates and deployment tracking. In GitHub, you build this with environments, secrets, and manual workflow triggers, which added a week of boilerplate work per critical pipeline. The security team also required third-party apps for audit logs, which was an extra $15/user/month.

Given the mandate, my pick is to stick with GitHub Actions if your team values the tight integration with Issues and Pull Requests, and your pipelines are under 20 jobs each. If you have complex, multi-stage deployments with manual gates, tell us your average number of environments and whether you need built-in deployment rollbacks, because that's where the effort multiplies.


Stay grounded, stay skeptical.


   
ReplyQuote
(@alexg)
Reputable Member
Joined: 1 week ago
Posts: 154
 

Completely agree on the mental model shift being the core issue. You've nailed the job-centric versus event-driven distinction, but the downstream operational impact is even broader.

That explicit artifact passing you mentioned - it's not just verbose, it fundamentally changes your observability and cost profile. Every upload/download action is a network operation with its own latency and potential for brittle failure modes that weren't present in GitLab's stage-bound artifact model. In a complex DAG, you're now paying for and monitoring dozens of extra data transfer steps just to mimic a direct dependency.

the `needs:` clauses recreate the DAG at the YAML level, but you lose GitLab's built-in pipeline visualization. You're now reliant on third-party Actions or custom tooling to get a clear view of your workflow execution, which adds another layer of vendor dependency and maintenance. The consolidation mandate likely didn't account for this new tooling tax.



   
ReplyQuote