How to Prepare for a Migration to GitHub Actions

Blog post

How to Prepare for a Migration to GitHub Actions

How to Prepare for a Migration to GitHub Actions

Most GitHub Actions migration projects don't fail at the conversion step. They fail because someone started converting before anyone knew what was actually in the estate.
We've seen the pattern plenty of times. A platform team gets the mandate, picks the pipelines they know, converts those, and then spends the next nine months discovering the other 1,400. Half of those turned out to be dead. A third of the live ones depend on a Groovy shared library nobody has opened since 2019. The one engineer who understood the release pipeline left in March.
Preparation is what stops that. It isn't project management overhead, and it isn't a six-week discovery phase with a slide deck at the end. It's four questions you answer before you convert anything, and most of the work of answering them is now automated.

What does preparing for a GitHub Actions migration actually involve?

Four things:
  1. What do you have, and what depends on it?
  2. What needs a human decision rather than a translation?
  3. How will identity and secrets work afterwards?
  4. Where will the workflows run?
That's it. Everything else in a migration plan is scheduling.
The conversion itself is no longer the hard part. AI agents write the workflow YAML now, and they write it well when you give them good context. The context is what you build during preparation, and it's the only part you can get wrong in a way that costs you a year.

Why start with an audit instead of a conversion?

Because a big chunk of a mature CI estate shouldn't be migrated at all. It should be deleted.
Pipelines accumulate. A job written for a service that was decommissioned two years ago still runs nightly. A release pipeline exists in three near-identical copies because three teams forked it in 2021. If you migrate all of that, you've reproduced the mess in a new system and you now own it in two places for the length of the transition.
So set the scope rules first, before anyone argues about them:
  • No successful run in the last 180 days? It's out of scope. Not a deletion candidate, not a "let's ask around". Out. If a team wants it back in, they can claim it and name an owner.
  • Never ran successfully at all? Also out. A pipeline that has never gone green isn't a pipeline, it's a draft. Migrating it just moves a broken thing to a new address.
  • No named owner? It's out until someone claims it. No owner means nobody to answer the agent's questions during conversion and nobody to test the result.
Those three rules alone routinely take a serious bite out of the inventory, and every pipeline they remove is one you don't have to analyze, map, convert, review or support.
What you want out of the audit is a list with a last successful run date, a named owner, a keep or drop decision, and a picture of what each surviving pipeline depends on.

What should a pre-migration analysis tell you?

It should tell you what exists, what depends on what, and what each change is going to touch, before anyone edits a single pipeline.
That's the part that's genuinely hard to do by hand, because the knowledge lives with two or three platform engineers and inside shared code nobody has read in years. It's why we built Pipeline Migration Assistant.
The importer runs as a Docker container against your live instance and pulls in the whole estate, not just the pipeline files:
  • From Jenkins: jobs and pipelines, plugins, credential definitions, shared libraries and relevant system configuration.
  • From Azure DevOps: YAML pipelines, Classic build and release definitions, tasks and templates, service connections, variable groups, secure file references, agent pools and environments.
  • From GitLab: pipelines from gitlab.com or a self-managed instance.
We don't import the values of your credentials. We import their names, and you map each one to a GitHub Secret during mapping.
Once the import finishes, the analysis runs on its own, in two layers:
  • Deterministic discovery. We parse the Groovy in your Jenkins pipelines and shared libraries, and we follow Azure DevOps template chains, to work out the real call structure: which jobs use which library functions and templates, and which referenced templates can't be found at all. Azure DevOps imports also get flagged for Classic pipelines, deprecated tasks and security anti-patterns.
  • AI best-practices analysis. A migration agent reads each pipeline and flags what would benefit from refactoring, like a hardcoded service endpoint, and flags every secret the pipeline uses with a recommendation to map it to an existing GitHub secret or create a new one.
The output is a complete, normalized inventory of your pipeline estate before you touch anything. Two parts of it matter most while you're still planning:
  • Impact before change. Every plugin carries a Used by list of the jobs and templates that reference it, so you can see the blast radius of replacing or retiring it before you commit to either. Every job carries a Dependencies list showing the migration status of everything it relies on. This is the data that tells the business what the migration will actually touch, which is usually the question that gets the project funded.
  • A worklist for developers and agents. Every job and shared element carries a status: Not Started, In Progress, Ready for Review, Migrated or Skipped. Skipped counts toward completion, because deciding not to migrate something is a decision, not unfinished work.
Import and analysis usually run in an hour or two. Kick it off at the start of the day and it's typically done by lunch. If you're a large enterprise with 1,000 or more pipelines, give it longer.

Which parts of a pipeline translate, and which need a decision?

More translates than people expect. The old advice that shared libraries and plugins are a wall you hit is out of date.
What you have
How it translates
What you need to decide
Standard build, test and publish steps
Converts cleanly
Spot-check the generated workflow
Jenkins shared libraries and Azure DevOps templates
Translate well. The agent analyzes the functional components and creates reusable workflows or Actions that do the same job
Which of them become the golden paths everyone calls
Common Jenkins plugins
Usually map straight to a marketplace action. The Jenkins Docker plugin, for example, can be replaced with docker/build-push-action almost every time
Nothing, unless you want a different action
Marketplace tasks with no GitHub Marketplace equivalent
Converted automatically into a shell step in the generated workflow
Whether a shell step is good enough long term, or you want a proper action written
Credentials from an external secret store
The reference converts. The trust model does not
OIDC now, or a second pass through every workflow later
Steps that depend on a mutable agent filesystem
Converts fine, then fails at run time
Fix the assumption. The analysis finds these
The pattern is simple: the code translates, the assumptions don't. Anything your pipeline relies on that isn't written down in the pipeline is where the thinking goes.
Migration Assistant turns that table into a mapping wizard. Legacy jobs get matched to GitHub repositories with fuzzy matching and bulk operations, plugins and tasks get mapped to GitHub Actions, credentials get mapped to GitHub Secrets at repository or organization scope, and source triggers and environments get mapped to their GitHub equivalents. Mappings export to an Excel workbook, so a team that doesn't live in CodeCargo can fill in its own rows and hand them back for import.

How do a thousand pipelines become a handful of reusable workflows?

By deciding the shared workflows before you migrate the pipelines that will call them.
Your organization might have 1,000 pipelines, but there are only 20 to 30 unique tasks: build a Docker image, run the test suite, deploy to Kubernetes, open a Jira ticket. Migrate those 1,000 pipelines one for one and you've got 1,000 implementations of 30 things. Migrate them onto shared reusable workflows and you still have 1,000 pipelines, but they each call one of the same few. That's what makes the estate maintainable, and governable, afterwards.
Here's the part people get subtly wrong. The thing you're looking for isn't a duplicate pipeline. It's the logic those pipelines share, and that logic usually already exists somewhere: in a Jenkins Shared Library, in an Azure DevOps pipeline template, in a script three teams copied.
And you don't replace two similar caller pipelines with one reusable workflow. You create a brand new reusable workflow that holds the shared logic, and both caller pipelines call it. It's a subtle difference and it changes the whole shape of the work. The callers stay callers. They get shorter, they keep their own triggers and inputs, and the logic they used to duplicate now lives in one place with one owner.
Migration Assistant looks for these patterns during mapping. Shared-library functions and templates that many jobs call are surfaced as candidates for Building Blocks, our reusable workflow components, and each confirmed pattern gets a blueprint describing the block's name, destination repository and its interface of inputs, outputs and secrets. When a batch runs, the blueprints are built first and the jobs that call them wait until the block exists.
For preparation, the question to settle is short: which golden paths will every team migrate onto, and who owns them? Answer it before the first wave.

How should secrets and identity be handled during the migration?

Treat the migration as the moment to stop copying long-lived credentials around.
The path of least resistance is to move each secret from the old store into GitHub secrets and carry on. That works, and it reproduces exactly the model you already have: static credentials, broad scope, no natural expiry, now in a second place.
The alternative is OpenID Connect federation, where the workflow asks the cloud provider for a short-lived token at run time and proves its identity instead of presenting a stored password. It's more work during the migration and a lot less work forever afterwards. Do it later and you're touching every workflow twice.
Migration Assistant helps here in a specific way. Because we import credential names and never their values, every Jenkins credential and Azure DevOps service connection arrives as a definition that has to be mapped on purpose. Credentials end up mapped explicitly to GitHub Secrets, not copied forward as mystery strings. That mapping list doubles as your OIDC worklist: every cloud credential on it is a candidate for federation instead of a stored secret.
Decide three things before conversion starts: which cloud roles each workflow needs, whether those roles are scoped per repository or shared, and who approves a workflow gaining a new permission.

Where will the workflows run, and who can reach what?

GitHub-hosted runners take a maintenance burden off you. Self-hosted runners give you network position and hardware control, and they inherit the blast radius of everything else that runs on them.
A self-hosted runner that can reach production is reachable by any workflow scheduled onto it, including one added by a pull request from a contributor you've never met. That's a network policy question, and it belongs in the migration plan rather than in a follow-up project, because runner placement gets decided once and then inherited by every workflow you write afterwards.
We built CargoWall for this layer, an eBPF-based L4 firewall for runners, and our migration practice moves teams onto GitHub-hosted runners wherever that's the right answer.

Who does the migration work: developers or agents?

Both, and good preparation is what lets you choose per pipeline.
With the analysis and the mappings in place, Migration Assistant runs the conversion as a big bang or in batches aligned to applications, teams or business units. Each pipeline gets its own AI migration agent in its own sandbox, with the pipeline source code, the pre-migration analysis, the recommendations your team added and the migration mappings as context. The agent generates the workflow, reviews its own output against your code standards and compliance guardrails, and opens a pull request. When it needs a decision it asks, through the CodeCargo platform, Slack or email, and waits for your answer.
Every pull request then goes through whatever review you want. An engineer can review it, or GitHub Copilot, or another AI coding agent. If your DevOps team would rather validate after merge, CodeCargo can approve and merge the pull requests in bulk. Engineers who prefer to work pipeline by pipeline can open any job in our editor in migration mode and convert it there with the same mappings as context. A migration dashboard tracks the status of every pipeline throughout.
Two things need an owner before the first batch starts: the batch boundaries, meaning which teams go in which wave and around which code freezes, and who reviews the pull requests.
Already moved your repositories with gh ado2gh? Migration Assistant links imported pipelines to their migrated repositories from the Migration Log, and its Prepare Rewire flow finds lingering Azure DevOps references, including pipeline resource declarations, .gitmodules entries and hardcoded clone URLs, and opens fix pull requests before you run gh ado2gh rewire-pipeline.

The five-gate migration readiness check

Don't schedule a conversion sprint until all five are true.
  1. Scope gate. Every pipeline has a last successful run date and a named owner, anything with no successful run in 180 days or no successful run ever is out, and what's left has its dependencies mapped.
  2. Translation gate. Every plugin and task has a mapped action, a planned reusable workflow, an accepted shell-step conversion or an accepted rewrite cost.
  3. Identity gate. The secrets model for after the migration is decided and written down, OIDC or otherwise, and every credential has a mapping.
  4. Runner gate. Every workflow class has a runner target, and every self-hosted target has a network policy.
  5. Rollback gate. For each pipeline in the first wave, you can say exactly how to put traffic back on the old system.
Teams that clear all five convert fast, because there's nothing left to discover. Teams that skip the first two spend the project discovering it under a deadline. With automated analysis, the first three gates are a review of work that's already been done rather than a research project.

How long should preparation take?

It scales with the estate, not with ambition. A few dozen pipelines with clear ownership is a couple of weeks. Several thousand across many teams takes longer, and the audit is what tells you which one you are.
The useful signal is the drop rate. When teams start returning "retire" on a real share of the inventory, the audit is doing its job. When every pipeline comes back as "keep, owner unknown", it isn't finished.

Where CodeCargo fits

Two ways.
  • Pipeline Migration Assistant, in the CodeCargo platform. Import from Jenkins, Azure DevOps or GitLab, automated dependency discovery and best-practices analysis, the mapping wizard, Building Block blueprints, and AI migration agents that open a pull request per pipeline, with a dashboard tracking every pipeline through the migration. Read Meet Pipeline Migration Assistant, the Pipeline Migration Assistant Deep Dive for a worked Jenkins example, or the Migration Assistant documentation.
  • Migration services. Our engineers run it with you: pipeline translation from Jenkins, GitLab CI, CircleCI and more, phased migration with validation and rollback at every step, and standardized reusable workflows so the estate doesn't fragment again (Services). If part of your estate is already on GitHub Actions, the no-cost GitHub Actions Assessment gives you the same picture of that side.
If you'd rather run it yourself, the five gates above are the whole method. If you want to see it work on your own pipelines, contact us.

Conclusion

The conversion is the easy part now. Preparation is where a migration is won or lost: cut the dead pipelines out of scope, find out what depends on what, decide the shared workflows, settle identity and runner placement deliberately, and make sure you can roll back. Do that and the conversion is a scheduling exercise.

Key Takeaways

  • Set scope rules before you audit. No successful run in 180 days, or no successful run ever, means out of scope.
  • Analyze dependencies, not just files. The impact of a migration lives in shared libraries, templates and plugins, and a dependency graph shows it before anything changes.
  • Shared libraries and most plugins do translate. What doesn't translate is the assumptions your pipelines make about the machine they run on.
  • Standardize first, migrate second. Build a new reusable workflow for the shared logic and have the callers call it. Don't merge the callers into each other.
  • Migrate identity, don't copy it. OIDC federation during the migration saves you touching every workflow a second time.
  • Runner placement is a security decision. A self-hosted runner is reachable by everything scheduled onto it.
  • Rollback is a precondition. A half-migrated estate still has to ship.

Key Terms

  • Pipeline Migration Assistant. The CodeCargo platform feature that imports legacy CI/CD data from Jenkins, Azure DevOps or GitLab, maps its dependencies, guides mapping to GitHub, and converts pipelines to GitHub Actions with AI migration agents.
  • Migration mapping. A decision about where a legacy component lands on GitHub: a repository, a GitHub Action, a GitHub Secret or a reusable workflow.
  • Building Block. CodeCargo's reusable workflow component, a wrapper around reusable workflows and GitHub Actions.
  • Reusable workflow. A workflow that other workflows call, so shared logic is defined once instead of copied into every pipeline.
  • Caller pipeline. The workflow that calls a reusable workflow. It keeps its own triggers and inputs and hands the shared logic off.
  • Composite action. Several steps packaged as one action, a common landing place for a shared-library function.
  • Jenkins Shared Library. Reusable Groovy shared across Jenkins pipelines, and usually the best place to find the logic a reusable workflow should hold.
  • OIDC federation. Short-lived, identity-based cloud credentials issued per workflow run instead of stored secrets.
  • Self-hosted runner. A machine you operate that executes workflows, with your network position.
  • Blast radius. What breaks if a given component is changed, retired or compromised.

FAQ

Which pipelines should we leave out of the migration? Anything with no successful run in the last 180 days, and anything that has never run successfully at all. Anything with no named owner stays out until someone claims it. That usually removes a large share of a mature estate before you spend a minute analyzing it.
Do Jenkins shared libraries have to be rewritten by hand? No. They translate well. The AI analyzes the functional components and creates reusable workflows or Actions that do the same work, which is also where you get the standardization benefit.
Do Jenkins plugins convert? Most of the common ones map straight to a marketplace action. The Jenkins Docker plugin, for instance, can be replaced with docker/build-push-action almost every time. Where a task has no equivalent in the GitHub Marketplace, it can be converted automatically into a shell step in the generated workflow.
What does Pipeline Migration Assistant import? From Jenkins: jobs and pipelines, plugins, credential definitions, shared libraries and relevant system configuration. From Azure DevOps: YAML pipelines, Classic build and release definitions, tasks and templates, service connections, variable groups, secure file references, agent pools and environments. GitLab is supported as a source too, on gitlab.com or a self-managed instance. Dependencies between everything it imports are mapped automatically.
Does Migration Assistant import our secret values? No. Only metadata and configuration. Credentials arrive as definitions without their values, and you recreate each value as a GitHub Secret during mapping.
Can AI agents do the migration work? Yes, with review. Each pipeline gets its own migration agent and sandbox, the agent asks when it needs a decision, and every result arrives as a pull request for an engineer or another coding agent to review.
Can we migrate incrementally? Yes, and you should. Run both systems during the transition with a clear rule for which one is authoritative per pipeline, migrate in batches by application, team or business unit, and keep rollback available for each wave.
What usually causes a migration to overrun? Scope nobody pruned, and pipelines with no owner. Both are visible in the audit and invisible if you skip it.
Should we move to GitHub-hosted runners at the same time? If the workflows don't need your network position, yes. You're already changing the pipeline, and it takes a maintenance burden off you. Where self-hosted is genuinely required, decide the network policy as part of the migration.
Does a migration improve our compliance position? It improves the evidence you can produce, because workflow runs are auditable and policy can be enforced on every run. It does not make an organization compliant on its own.
C

CodeCargo Team

The CodeCargo team writes about GitHub workflow automation, developer productivity, and DevOps best practices.