CI/CD Migration · Release Engineering
Migrating from Jenkins to GitHub Actions Without Stopping Releases
A pipeline-by-pipeline plan for moving off Jenkins while your team keeps shipping on its normal schedule.
Executive brief
- The safest Jenkins to GitHub Actions migration is incremental. You move one pipeline at a time, run old and new side by side in shadow mode, and cut over only after the outputs match. A big-bang cutover is the thing that stops releases.
- Start with an inventory of every job, plugin, credential, and agent label. Automated tooling like GitHub Actions Importer helps with the audit and first drafts, but it doesn't finish the job.
- Separate build parity from publish authority. Both systems can build the same commit, but only one of them is allowed to publish a release artifact at any moment.
- Turn Jenkins shared libraries into reusable workflows, and replace long-lived stored credentials with OIDC where your cloud supports it.
- Keep Jenkins warm as the rollback path through at least two full release cycles after cutover.
- Most schedule risk lives in credentials, runner capacity, and undocumented plugin behavior. Converting the YAML is the easy part.
Why big-bang migrations stall releases
Most failed Jenkins migrations share the same shape. Someone decides the whole estate moves over a quarter, a branch of converted workflows appears, and a cutover date gets pinned to the calendar. Then the date arrives and it turns out the release pipeline depended on a plugin nobody remembered, a credential only one departed engineer knew about, and an agent with a hand-installed toolchain from 2019.
Jenkins tends to accumulate this kind of hidden state because it lets you. Configuration lives partly in Jenkinsfiles, partly in the UI, partly in shared libraries, and partly in the heads of people who set it up. Converting the files captures maybe half of what the system actually does.
The alternative is boring on purpose. You migrate in slices small enough that any single failure is cheap, and you never remove the old path until the new one has proven itself on real releases.
If the migration failed on a Friday afternoon, could you ship a hotfix from Jenkins within the hour without touching the new setup? If the answer is no, the plan is a big-bang plan, whatever it's called.
Start with an inventory, not a converter
Before writing any workflow YAML, list what you actually have. For each Jenkins job or multibranch pipeline, record the trigger, the agent label, the plugins it uses, the credentials it reads, what it produces, and where it publishes. Include the jobs nobody admits to owning. Those are usually the ones that break.
GitHub Actions Importer is worth running at this stage. It's a gh CLI extension with audit, forecast, dry-run, and migrate commands, and the audit output gives you a fast, honest picture of which pipelines convert cleanly and which use constructs it can't translate. Treat the converted workflows as a first draft that a person reviews. They're not a finished migration, and the ones that look finished are the ones to read most carefully.
Tier your pipelines
Not every pipeline deserves the same care. Sort them into three tiers and migrate in that order, because the early, easy wins are how you find out what your shared workflow layer is missing.
| Tier | What it looks like | Migration approach |
|---|---|---|
| Tier 1 | Build and test on pull request. No publishing, no production credentials. High run frequency. | Migrate first. Cutover is low risk, and you learn the platform on real traffic. |
| Tier 2 | Release and deploy pipelines. Publish artifacts, push images, touch production. | Shadow mode, artifact parity checks, then a scheduled cutover with Jenkins kept as fallback. |
| Tier 3 | Snowflakes: custom Groovy, obscure plugins, hardware-in-the-loop rigs, manual input gates, pipelines generated by scripts. | Redesign rather than translate. Some of these should be retired instead of moved. |
Tier 3 is where you'll find that a quarter of your jobs haven't run in eight months. Ask the owners. Deleting a dead job is the cheapest migration there is.
Can you name, without opening Jenkins, every credential your release pipeline reads and every system it can write to? If not, the inventory isn't done.
How Jenkins concepts map to GitHub Actions
The vocabulary differs, and a few of the differences bite. The big one: a Jenkins pipeline often assumes one agent and one workspace for the whole run, while a GitHub Actions workflow runs each job on a fresh runner. Anything that used to pass files between stages through the shared workspace now needs artifacts or caches.
| Jenkins | GitHub Actions | What to watch for |
|---|---|---|
| Jenkinsfile stages | Jobs and steps in a workflow | Each job gets a clean runner. Shared workspace assumptions break. |
| Shared library (Groovy) | Reusable workflows and composite actions | Groovy logic doesn't port. Rewrite it as explicit inputs and outputs. |
| Agent labels | runs-on labels and runner groups | Label sprawl carries over unless you clean it up on purpose. |
| Credentials store | Secrets, variables, and OIDC | Scope secrets to environments, not to the whole repository. |
| Parameterized build | workflow_dispatch inputs | The workflow file has to exist on the default branch to be dispatchable from the UI. |
| Cron trigger | schedule | Runs from the default branch only, and can start late under platform load. |
input step (manual approval) | Environment with required reviewers | Approval attaches to an environment, so design environments around your gates. |
| stash / unstash | upload-artifact / download-artifact, or cache | Artifact retention and size limits apply. |
| post { always / failure } | if: always() and if: failure() steps | Cleanup steps need explicit conditions. |
Shared libraries deserve their own paragraph, because they're where most of the hidden logic lives. A library that started as three helper functions often grows into a hundred-line Groovy framework that decides how every team builds and releases. Rewriting it as a small set of reusable workflows with typed inputs is more work than a straight port, and it usually leaves you with something people can actually read.
Pick one shared library function at random. Can a new engineer explain what it does, and what breaks if it changes, in five minutes? If not, port the behavior, not the code.
The migration playbook, step by step
This is the sequence we use when a team needs to keep shipping throughout. The order matters more than the timings, which depend heavily on how many pipelines you have and how tangled the shared library is.
- Inventory and tier every job Record triggers, plugins, credentials, agents, and outputs for each pipeline, then sort into three tiers. Retire anything nobody can justify.
- Stand up the runner platform first Decide between hosted and self-hosted runners, build the images your pipelines need, and set network access before any real pipeline moves.
- Build the shared workflow layer Turn the common parts of your shared library into reusable workflows and composite actions, with branch protection and environments configured.
- Port a low-risk pipeline and run it in shadow mode Start with a Tier 1 pipeline. Run the new workflow on the same commits as Jenkins, with publishing disabled or pointed at a scratch location.
- Prove artifact parity Compare test results, dependency lockfiles, file lists, and image contents between the two systems across a couple of weeks of real commits.
- Cut over publish authority, one pipeline at a time Move publishing permission to GitHub Actions on a scheduled date, disable the Jenkins publish step, and keep the Jenkins job runnable for rollback.
- Keep Jenkins warm, then decommission Leave Jenkins restorable through at least two full release cycles, archive build history and logs you need to keep, and then shut it down.
Steps 2 and 3 are the ones teams want to skip, and they're the ones that decide whether the rest goes quickly. If the runner platform is shaky, every pipeline you port inherits the shakiness and you'll spend the migration debugging infrastructure through the lens of individual workflows.
Shadow mode and publish authority
Running both systems in parallel is the core of a migration that doesn't interrupt releases. The risk is that both systems then try to do the same thing, and two systems publishing the same version tag is how you end up with a corrupted release or a confusing rollback.
The rule is simple. Build authority can be shared, publish authority can't. During shadow mode the new workflow builds and tests every commit, but it pushes only to a scratch registry or under a clearly different tag such as a -shadow suffix. Jenkins keeps its permission to publish real releases. On cutover day, you move that permission in one change and remove it from the other side.
What "parity" should mean
Byte-identical output is a nice goal, but most builds aren't reproducible enough for it, because timestamps, build paths, and dependency resolution all leak into artifacts. Insisting on identical hashes will send you chasing noise. Compare what actually matters instead: the same tests ran and passed, the same dependency versions resolved, the same files ended up in the package, the container image has the same layers of interest, and the version metadata matches.
Where a build is reproducible, hash comparison is a strong signal and worth using. Where it isn't, a diff of the file manifest and the resolved lockfile catches most real regressions.
At this moment, can you say which single system holds permission to publish each of your release artifacts? If the answer is "both, but we're careful," you have a duplicate-publish incident waiting.
Secrets and credentials
This is where migrations quietly lose weeks. Jenkins credential stores tend to hold a decade of long-lived tokens, some of them shared across dozens of jobs, some belonging to service accounts nobody can rotate without breaking something.
Don't copy them all across. Use the migration as the excuse to shrink the list. For each credential, find out which pipelines read it and whether it's still needed. For cloud access, prefer OpenID Connect so a workflow gets short-lived credentials from your provider at run time instead of storing a static key. GitHub documents this in its guide to security hardening with OpenID Connect, and AWS, Azure, and Google Cloud all support the pattern.
For anything that has to stay a stored secret, scope it to an environment such as staging or production, and put required reviewers on the production one. That replaces the Jenkins input approval step and gives you a cleaner record of who approved what.
Rotate at cutover. Once a credential has moved to the new system, the copy still sitting in Jenkins is a liability, so revoke it rather than leaving it around.
If a production credential leaked today, could you list every pipeline that uses it and rotate it in under a day, in both systems?
Runners and build capacity
GitHub-hosted runners are the fastest way to get going, and for many pipelines they're all you need. They don't reach into your private network, though, and they won't have the specialized toolchains, large disks, or hardware attachments that some builds depend on.
For those, you'll want self-hosted runners. The pattern that holds up best is ephemeral runners, one per job, managed by Actions Runner Controller on Kubernetes or a similar autoscaler, so nothing carries state between jobs. Long-lived self-hosted runners that get patched by hand drift exactly the way Jenkins agents did, and you'll have recreated the problem you were trying to leave.
Two capacity questions catch teams out. First, a Jenkins controller with a fixed pool of executors queues work politely, whereas a fan-out of matrix jobs on Actions can suddenly demand far more concurrency than you planned for, so check your concurrency limits and your runner autoscaling ceiling before you turn on a busy repository. Second, caching behaves differently. Jenkins agents kept warm workspaces and local package caches almost by accident, and on fresh runners you must set up dependency and build caching deliberately or your build times will get worse before they get better.
If your builds do hardware-in-the-loop testing or cross-compile for embedded targets, plan that as its own workstream. Those rigs usually stay self-hosted and need labels, locking, and scheduling thought that a standard web-service pipeline never does.
What happens to your queue if 40 pull requests land in the same ten minutes? If nobody knows, load test the runner pool before cutover, not after.
Anti-patterns that cause outages
| Anti-pattern | What goes wrong | Do this instead |
|---|---|---|
| Converting everything with a tool and cutting over | Unsupported plugins and Groovy logic fail silently or behave differently in production. | Use the tool for audit and drafts. Review and shadow-test every release pipeline. |
| Both systems can publish | Duplicate or conflicting release artifacts, and unclear rollback. | One publish authority at a time, moved in a single deliberate change. |
| Copying every credential across | You inherit years of over-scoped, unrotated secrets. | Inventory, prune, prefer OIDC, and revoke old copies at cutover. |
| Porting the shared library line by line | A large, opaque Groovy framework gets reborn as opaque YAML. | Port behavior into small reusable workflows with explicit inputs. |
| Decommissioning Jenkins at cutover | No fallback when the first hotfix hits an edge case. | Keep it restorable for at least two release cycles. |
| Skipping the runner platform work | Every migrated pipeline blames itself for infrastructure flakiness. | Stabilize runners and images before porting real pipelines. |
The audit and compliance angle
If you're under SOC 2, ISO 27001, or a similar framework, the migration touches your change management story, and auditors will want to see continuity. Jenkins probably holds the evidence of who approved and deployed what for the audit period, so archive that history and the relevant logs before the server goes away, and keep them for whatever retention your controls specify.
On the new side, branch protection, required reviewers on production environments, and the workflow run history give you a tidy approval and deployment trail, often a better one than Jenkins provided. Write down the mapping from old control to new control while you migrate, because it's far easier than reconstructing it during the next audit.
- Jenkins to GitHub Actions
- Shadow mode
- Publish authority
- Artifact parity
- Reusable workflows
- OIDC
- Ephemeral runners
- Rollback path
- Release engineering
FAQ
How long does a Jenkins to GitHub Actions migration take?
It depends on pipeline count and how much custom Groovy you carry. A team with a few dozen straightforward pipelines can often move in a couple of months alongside normal work, while a large estate with heavy shared-library logic and self-hosted infrastructure commonly takes two quarters or more. The runner platform and credentials usually set the pace, not the workflow conversion.
Can I run Jenkins and GitHub Actions in parallel?
Yes, and you should. Run the new workflow in shadow mode against the same commits, publish only to a scratch location, and keep publish permission for real releases with Jenkins until the outputs match. The one rule is that only one system can publish a given artifact at a time.
Does GitHub Actions Importer convert everything automatically?
No. It audits your Jenkins instance, forecasts effort, and produces draft workflows, but plugins without an equivalent, custom Groovy, and some pipeline steps need manual work. Treat its output as a first draft that a person reviews.
What replaces Jenkins shared libraries?
Reusable workflows and composite actions cover most cases. Reusable workflows suit whole pipeline stages with inputs and secrets, while composite actions suit smaller repeated step sequences. Logic that was clever Groovy usually gets simpler when rewritten as explicit inputs.
Should we use GitHub-hosted or self-hosted runners?
Use GitHub-hosted runners where your builds don't need private network access or special hardware. Use self-hosted runners, ideally ephemeral ones managed by Actions Runner Controller on Kubernetes, for internal network access, large builds, or embedded and hardware-in-the-loop testing.
When can we shut Jenkins off?
After the new pipelines have carried at least two full release cycles in production without a rollback to Jenkins, and after you've exported or archived the build history and audit logs your compliance obligations require. Keep the server in a stopped but restorable state for a while longer.
Planning a Jenkins migration you can't afford to get wrong?
We've moved release pipelines off Jenkins for teams that ship weekly and can't pause. If you want a second pair of eyes on your inventory, your cutover plan, or your runner design before you commit, talk to us.
Talk to Stonetusker about your migration
