← All case studies GitHub → GitLab

Outschool mirrored GitHub while GitLab became the place merges landed

Outschool imported GitHub into GitLab in 2022, then mirrored merge requests back so GitHub Actions and Heroku review apps kept running. A few senior engineers finished the wider move in under three months.

Jimmy Berry wrote Heroku Exodus and Gitlab Migration for Outschool. The page was published on 10 January 2022 and edited again in March 2023. One post covers two moves. GitHub to GitLab came first, because that was how they could leave Heroku for AWS.

This note stays on the GitLab cutover. The Heroku half is in the same essay if you need ECS Fargate, RDS, and the Cloudflare load balancer.

The GitHub → GitLab playbook is the import, the identities, and the CI you will have to rebuild. Outschool’s trick was to keep GitHub alive as a mirror while that rebuild happened.

Import, then stop reviewing on GitHub

They did a full import of the GitHub project into GitLab. After that, source changes and reviews happened on GitLab.

A GitLab CI job pushed the review branch back to GitHub and opened a pull request there. The script titles that pull request with a [GITLAB] prefix and points the body at the GitLab merge request. The pull request number is written to a dotenv file, cached, so the next pipeline can tell that the pull request already exists.

A GitLab environment named mirror/... uses that number as its URL. Opening “view latest app” on the mirror environment lands you on the GitHub pull request, which is where the old GitHub Actions checks still showed up.

When the merge request closes, a stop job deletes the GitHub branch. GitHub then closes the pull request.

Heroku review apps were still created by the GitHub integration. A no-op GitLab job turned the mirrored pull request number into a Heroku review URL, so a developer could open that app from the GitLab merge request too. The hostname in the essay is a sample, example-staging-pr-$GITHUB_PR.herokuapp.com. Treat it as the pattern, not a live Outschool host.

Port the workflows one merge request at a time

Once the mirror was up, they moved GitHub Actions to GitLab CI in slices. A merge request could delete an Actions workflow and add the GitLab job in the same change. From the author’s side, more of the result showed up on the GitLab merge request, and there was less reason to open GitHub.

Child pipelines took the place of an AWS CodePipeline. A CDK synthesizer they already had emitted GitLab CI jobs instead, so those stacks did not have to be rewritten before the runner changed. The same trick applied one job template to many directories and ran only the ones a merge request touched.

They also built a CI image with the tools those jobs kept installing, and they ran their own runners on AWS with the npalm/gitlab-runner/aws Terraform module. Heavy jobs were tagged onto the larger machines. Shared GitLab runners stayed available for the rest.

How long, and what that number includes

Berry says a few senior engineers finished in under three months. Consensus had taken about a month before that. Ordinary interruptions are included in the span. The span covers the GitLab move and the Heroku-to-Fargate move together.

The developer loop he prints is the part I would copy. Push to GitLab, open a merge request, merge, promote, all without a second site. They only got there after the mirror had carried Actions and Heroku through the middle weeks.

If GitHub Actions has to stay up on day one, import the project and mirror pull requests back until each workflow has a GitLab replacement. The playbook covers the import limits. Berry published the mirror scripts he used in those weeks.