Skip to content
Back to Blog
ci-cdjenkinsgithub-actionsdevopspipeshift

How We Migrated from Jenkins to GitHub Actions (And What We Learned Building Pipeshift)

A practical account of migrating production CI/CD from Jenkins to GitHub Actions — the decisions, the tradeoffs, and the patterns that didn't survive the translation.

Full disclosure: I'm building Pipeshift, a tool that helps teams migrate CI/CD pipelines using AI-assisted analysis. The patterns in this post come from the migrations I've done that informed Pipeshift's design. I have an obvious interest in the problem space, and I'll try to be honest about where that shapes my perspective.


Jenkins to GitHub Actions is the most common CI/CD migration I get asked about. The surface-level narrative is simple: Jenkins is complex, GitHub Actions is simpler, everyone should migrate. The reality of migrating a production Jenkins setup with years of accumulated configuration is substantially less clean.

Here's what actually happened across several migrations, and what the patterns taught me about what's hard to automate.

What Jenkins does that GitHub Actions doesn't do the same way

Before getting into migration patterns, it's worth being precise about what you're actually changing, because the mental model difference is significant.

Jenkins is a CI server: a long-running process that manages job state, has a plugin ecosystem for extending behavior, and treats pipelines as stateful processes. A Jenkins job can be paused, resumed, have input gates, maintain workspace state across runs, and be triggered by arbitrary external events through plugins.

GitHub Actions is a workflow orchestrator: stateless by default, event-driven, declarative YAML that runs on GitHub-managed infrastructure (or your own runners). Each run is isolated. There's no persistent state between runs unless you explicitly store it (artifacts, caches).

This difference matters for migration because patterns that are natural in Jenkins don't translate directly to GitHub Actions — they translate to a different equivalent pattern, which requires understanding intent rather than just converting syntax.

The patterns that don't survive direct translation

Shared libraries. Jenkins shared libraries are Groovy code in a separate repository that can be imported into Jenkinsfile pipelines. Teams use them to share common pipeline logic — deployment functions, notification helpers, validation steps — across multiple projects.

GitHub Actions has reusable workflows and composite actions, which serve a similar purpose but have different invocation mechanics. The direct translation is to convert each shared library function into a composite action. The practical difficulty: Jenkins shared library functions often use Jenkins-specific APIs (current build context, workspace manipulation, plugin-provided steps) that have no direct equivalent. You're not translating code — you're re-implementing intent.

Input gates. Jenkins supports input() steps that pause a pipeline and wait for a human approval before proceeding. Teams use these for deployment approvals: the pipeline builds and tests, pauses, waits for approval, then deploys. This pattern is fundamental to many organizations' change management processes.

GitHub Actions has environment protection rules, which provide deployment approval gates. The mechanics are different — in Jenkins, the pause is inline in the pipeline; in GitHub Actions, it's a property of the target environment. The outcome is similar, but the implementation model is different enough that teams often don't realize they need to configure environment protection rules as a separate step from converting the pipeline YAML.

Artifact management. Jenkins workspaces persist between stages (within a run) and can optionally persist between runs. Teams sometimes rely on this persistence for build artifacts, test reports, or intermediate outputs without explicitly archiving them.

GitHub Actions has no persistent workspace between jobs by default. Anything that needs to move between jobs must be explicitly uploaded as an artifact and downloaded in the next job. This is architecturally cleaner (explicit dependencies) but it requires intentional changes to any pipeline that relies on implicit workspace persistence.

The migration approach that works

The approach I've found most reliable is intent-first rather than syntax-first.

For each Jenkins pipeline, the question isn't "how do I convert this Groovy to YAML?" — it's "what is this pipeline trying to accomplish, and what's the simplest GitHub Actions workflow that accomplishes the same thing?"

That reframing is important because production Jenkinsfiles often have accretions — code added to work around Jenkins behavior that isn't needed in GitHub Actions, plugins that solve problems GitHub Actions handles natively, conditional logic that compensated for Jenkins limitations.

The migration process I use:

  1. Document the pipeline's intent: what triggers it, what it produces, what constitutes success, what gates exist between stages.
  2. Map each stage to a GitHub Actions equivalent, noting where the mapping isn't direct (shared library calls, input gates, workspace persistence).
  3. Build the GitHub Actions workflow from scratch based on that intent mapping — don't copy-paste Groovy and try to convert it line by line.
  4. Run both pipelines in parallel for a period (Jenkins on the existing branch, GitHub Actions on a feature branch or fork) and compare outputs.
  5. Cut over when the GitHub Actions pipeline produces equivalent results for a representative set of real commits.

Step 4 is the one teams skip most often. Running in parallel feels expensive — it requires maintaining both systems simultaneously. But it's the only reliable way to catch the cases where the GitHub Actions pipeline produces a different result not because of a bug you introduced, but because the behavior difference between the two systems surfaces a latent assumption in your pipeline.

The Pipeshift angle

Pipeshift automates steps 1 and 3 of that process: analyzing existing Jenkins pipelines to extract intent, and generating GitHub Actions workflow YAML. The tool is informed by the failure patterns I've seen in manual migrations — specifically, the shared library translation problem and the input gate equivalence problem.

What Pipeshift can't automate: the parallel running period, the organizational decisions around environment protection rule setup, and the cases where the Jenkins pipeline's intent is unclear because it was written by someone who is no longer on the team. Those require human judgment.

I'm not claiming Pipeshift solves the migration problem entirely — it reduces the mechanical translation work so that engineers can focus on the parts that actually require judgment.

What I've learned about CI/CD migration as a category

The migrations that go well have one thing in common: the team treats it as a re-implementation of the pipeline's intent, not a translation of the pipeline's code. The migrations that go poorly treat it as a syntax conversion and are surprised when the output behavior differs.

A Jenkins pipeline that has been in production for several years is not just code — it's an accumulation of institutional knowledge about what the build system needs to do, encoded in a form that's now tightly coupled to Jenkins-specific behavior. That knowledge needs to be extracted and re-expressed, not transliterated.

The tooling can help with the mechanical work. The judgment work is still yours.


If your team is mid-migration and hitting specific translation problems, get in touch — I've probably seen the pattern before.