DEV Community

Cover image for Roku Just Moved from GitHub to GitLab. How to Migrate Your Repos Too
Alan West
Alan West

Posted on Originally published at blog.authon.dev

Roku Just Moved from GitHub to GitLab. How to Migrate Your Repos Too

On October 16, 2026, Roku posted a short notice on its developer forum: the developer docs, sample apps and tooling now live at gitlab.com/rokudev. Almost every repo under github.com/rokudev was archived the night before. No reason was given, and when someone asked in the thread, nobody from Roku answered.

If you've been wondering whether you could do the same, the answer is yes, and the git part is the easy bit. Below is the full GitHub to GitLab migration I'd run today: the importer and its token quirks, issues and PRs, rewriting GitHub Actions as .gitlab-ci.yml, and a push mirror so your old GitHub links keep working. At the end there's a list of the things that don't come across at all.

Roku isn't alone, either. Zig left for Codeberg in late November 2025 and blamed GitHub Actions directly, and the Hacker News thread on Roku's move hit 288 points and 203 comments.

Git was never the thing locking you in

Moving a git repo takes one command. git push --mirror has worked since forever. If git were what held you to GitHub, everyone would have left years ago.

What holds you is everything wrapped around the repo:

  • Actions. Hundreds of lines of YAML that call uses: some-org/some-action@v3, and none of it runs anywhere else.
  • Issues and PRs. Years of review comments, links and history that live in GitHub's database. Your clone doesn't have them.
  • Trust in the defaults. On March 25, 2026, GitHub announced that starting April 24, Copilot interaction data from Free, Pro and Pro+ users would be used to train its models unless the user opts out. That covers code snippets, the code around your cursor, file names, repository structure and navigation patterns. Business and Enterprise plans are excluded. Private repos "at rest" are excluded too, but code Copilot processes during a session can be used.

One caveat on that last one: moving your repos to GitLab doesn't change Copilot's terms. If you keep using Copilot on a Free or Pro plan, your interaction data is covered wherever the code is hosted. The opt-out is what protects you. What the change did was show that GitHub will flip a default on your code and leave it to you to find the switch, and for some teams that's reason enough to stop treating GitHub as the only option.

The HN thread is mostly about the first point. One commenter called Actions "a god-awful mess" and said GitLab's CI/CD is "leaps and bounds over GitHub Actions". GitLab got plenty of criticism too: cross-group permissions, no group reviewers, a sidebar full of Secure/Deploy/Operate menus nobody asked for. What I didn't see anyone argue is that hosting the git data itself is hard to replace.

Whether you stay or go, if you use Copilot on a personal plan: open github.com/settings/copilot, go to the Privacy section and turn off training on your data.

How to migrate from GitHub to GitLab, step by step

For a normal-sized repo (a few thousand issues, a CI file or two), one hour is realistic. For a monster it isn't: GitLab's own docs say importing the Kubernetes repo, with about 80,000 PRs, 45,000 issues and 1.5 million comments, took 76 hours. Imports are also subject to GitHub's API rate limit, usually 5,000 requests per hour.

1. Create the right GitHub token

The GitHub importer supports classic personal access tokens only. Fine-grained tokens won't work. Scopes:

  • repo (required)
  • read:org if you want collaborators or LFS objects imported

2. Plan for placeholder users

Since GitLab 17.8 the importer maps contributions after the import. Every GitHub author lands on a placeholder user that can't sign in or be a member of anything. A group Owner then goes to Manage → Members → Placeholders in the top-level group and reassigns each placeholder to a real GitLab account, and that person has to accept. Once accepted, later imports from the same source into the same group map that user automatically.

So: get your team to create GitLab accounts before migration day, and budget time for the reassignment round afterwards. The older email-matching method (public GitHub email = GitLab email) only exists behind a feature flag on Self-Managed and Dedicated 18.7 and earlier. It isn't available on GitLab.com.

3. Run the import

In the UI: Create new (+) → New project/repository → Import project → GitHub, authorize, pick repos. For scripting it, or for GitHub Enterprise Server, use the import API:

curl --request POST \
  --url "https://gitlab.com/api/v4/import/github" \
  --header "content-type: application/json" \
  --header "Authorization: Bearer <your_gitlab_token>" \
  --data '{
    "personal_access_token": "<github_classic_pat>",
    "repo_id": "12345",
    "target_namespace": "your-group",
    "optional_stages": {
      "attachments_import": true,
      "collaborators_import": true
    }
}'
Enter fullscreen mode Exit fullscreen mode

repo_id is the numeric GitHub repository ID, not the name. Get it with gh api repos/OWNER/REPO --jq .id (for rokudev/hello-world that returns 50557099).

What comes across: all branches, LFS objects, issues, PRs with reviews and review comments, labels, milestones, release notes, wiki pages and branch protection rules. Markdown attachments only come across if you enable that option. It's off by default because it slows imports down, but if you skip it, the images in your issues stay on GitHub instead of moving with the project.

4. Rewrite GitHub Actions as .gitlab-ci.yml

Nothing converts this for you. The mapping from GitLab's migration guide is short:

GitHub Actions GitLab CI
on: per-job rules:
runs-on: tags: (runner selection)
steps: script:
uses: include: / plain shell
env: variables:
strategy: matrix: parallel: matrix:
${{ secrets.NAME }} $NAME (masked CI/CD variable)
actions/cache cache:
actions/upload-artifact artifacts:

A typical Node workflow:

# .github/workflows/test.yml
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

becomes:

# .gitlab-ci.yml
test:
  image: node:$NODE
  parallel:
    matrix:
      - NODE: ["20", "22"]
  cache:
    key:
      files: [package-lock.json]
    paths: [.npm/]
  script:
    - npm ci --cache .npm --prefer-offline
    - npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH
Enter fullscreen mode Exit fullscreen mode

Keep the when: never line. Without it, a push to a branch with an open merge request starts two pipelines, one for the branch and one for the MR.

Checkout is implicit, and the Docker image replaces setup-node. That's the pattern for most of your uses: lines: an action from the marketplace usually turns into a container image plus two shell lines. The painful cases are actions with real logic in them, like a deploy action written in TypeScript. Those you rewrite by hand.

Secrets go into Settings → CI/CD → Variables, marked masked and protected. GitLab's docs say outright: never put them in .gitlab-ci.yml.

5. Mirror back to GitHub so old links keep working

GitHub won't redirect to another host. Your stars, inbound links and go get paths all point at github.com. The cheap fix is a push mirror from GitLab to the old GitHub repo:

  1. On GitHub, create a fine-grained token with read/write on Contents (and Workflows if the repo still has a .github/workflows directory).
  2. In GitLab: Settings → Repository → Mirroring repositories.
  3. URL https://github.com/OWNER/REPO.git, direction Push, username = token owner, password = token.
  4. Tick Only mirror protected branches. GitLab then pushes about every minute instead of about every five.

Then delete .github/workflows so the mirror doesn't run CI on GitHub too, disable Issues and PRs on GitHub, and put a "moved to GitLab" line at the top of the README.

Roku went the other way and archived its GitHub repos (a handful, like lt-sdk and roku-deploy, are still live). Archived repos are read-only, so they can't receive mirror pushes. Your old URLs keep working, but the code there is frozen. For a docs repo that third parties crawl, that's the worse choice: Roku's own notice tells anyone indexing their repos "for a knowledge base, assistant, or coding agent" to re-crawl, because raw file URLs changed.

6. Update every clone

git remote set-url origin https://gitlab.com/OWNER/REPO.git
# submodules: edit .gitmodules, then
git submodule sync
Enter fullscreen mode Exit fullscreen mode

What doesn't survive the move

Straight from the importer docs and from what's missing in them:

  • Organizations and groups. "Importing projects does not migrate or import any types of groups or organizations." You rebuild the group tree by hand.
  • Outside collaborators. Not imported.
  • "Require status checks" branch rule. Dropped, so you recreate it as an external status check.
  • Custom repository roles. These cause partially completed imports.
  • Actions, webhooks, GitHub Apps. Not mentioned in the importer docs, and none of it comes across.
  • PRs opened after the import. Mirroring doesn't sync them, so freeze the GitHub side on migration day.
  • Stars, forks, the social graph. Gone for good. For an open source project that's the real cost, and the reason many maintainers mirror instead of leaving outright.

My take

Leaving GitHub is no longer hard. The GitLab importer is good, and you'll lose an afternoon on Actions YAML, a round of placeholder reassignments and maybe a few stars. The reason to bother has changed too: after a year of Actions complaints and a training default flipped to opt-out, "we've always been on GitHub" stops being a neutral choice.

Roku won't say why it left. I'd bet the answer is boring, like standardizing on the GitLab they already run internally, as a couple of HN commenters guessed. Even a boring reason tells you something: a big public company decided GitHub's network effect wasn't worth keeping even for its public developer repos.

What would actually make you leave GitHub: pricing, Copilot training on your code, Actions outages, or nothing ever? And if you've already moved, what broke that the docs didn't warn you about?

Sources

Top comments (0)