Migrating CI/CD pipelines is a common task for DevOps engineers, especially as teams move toward more unified and modern delivery platforms. In this real-world case study, we walk through the process of migrating multiple Bitbucket repositories and pipelines to GitHub, while modernizing CI workflows using GitHub Actions.

The repositories in scope were responsible for firmware builds based on OpenWRT and Yocto, two widely used Linux-based frameworks for embedded systems. The goal was not only to migrate source code, but also to consolidate fragmented pipeline implementations into a single, consistent CI/CD architecture.

This article is Part 1 of a migration series, focusing on repository migration and preserving Git history, structure, and dependencies.

The repositories involved in this migration had different CI implementations:

One of the key challenges was unifying these approaches under a single CI/CD platform. Migrating everything to GitHub and standardizing on GitHub Actions provided a clean and maintainable foundation for future development.

The migration process consisted of three main goals:

  1. Move repositories from Bitbucket Cloud to GitHub Cloud
  2. Preserve full Git history, including branches, tags, and references
  3. Ensure submodules and dependencies continue to function correctly

Because this was a Bitbucket Cloud environment, not all migration tools were applicable. A direct Git-based approach was selected to ensure full control and transparency during the process.

For each project, an empty repository was created in GitHub using the same repository name as the original Bitbucket repository. This repository served as the migration destination.

bitbucket

To preserve the complete repository state, including all branches and tags, a mirrored clone was created:

git clone --mirror git@bitbucket.org:workspace/project-repo.git

This creates a bare repository containing all Git objects and references, without checking out a working directory. A typical structure looks like:
branches  config  description  HEAD  hooks  info  objects  packed-refs  refs

This confirms that the full Git history is available locally.

Next, the mirrored repository was pushed to GitHub:

cd project-repo.git
git push --mirror git@github.com:workspace/project-repo.git

This ensures that all branches, tags, and references are transferred exactly as they existed in Bitbucket Cloud.

Once the repository was pushed, several checks were performed:

If necessary, the default branch can be updated in GitHub via:

Repository → Settings → General → Default branch

bitbucket

These checks ensure the repository is ready for CI/CD pipeline migration.

Many of the migrated repositories relied on Git submodules. After migration, all submodule URLs needed to be updated to point to GitHub instead of Bitbucket.

Before migration:

[submodule "submodule_name"]
  path = submodule_path
  url = git@bitbucket.org:workspace/dependant-repo.git

After migration:

[submodule "submodule_name"]
  path = submodule_path
  url = git@github.com:workspace/dependant-repo.git

Once updated, initialize and update submodules:

git submodule update --init --recursive

In some cases, submodules themselves contained nested submodules. These nested dependencies might still reference old Bitbucket URLs.

To ensure Git’s internal configuration is fully updated, run:

git submodule sync --recursive

This step is especially important when working with previously initialized repositories.

Some submodules were pinned to specific commit SHAs to ensure build stability. Preserving these references is critical for firmware builds and reproducible pipelines.

Steps to preserve a specific commit:

cd submodule_path
git checkout <commit-sha>

Then return to the main repository:

git add submodule_path
git commit -m "Preserve submodule at specific commit SHA"
git push origin <branch-name>

This ensures the repository continues using the exact submodule version required by the build process.

This concludes Part 1 of our migration journey series.

In the next post, we’ll focus on: updating project dependencies securely, ensuring that builds and pipelines we plan to migrate continue to function without disruption. Stay tuned for Part 2!


Check out more of our blog posts here.