GitLab → GitHub Contributions Sync

January 2026 → January 2026

Code source

When you work on a company GitLab, your GitHub contribution graph stays empty. Months of work invisible on the profile people actually look at. This tool pulls commit activity from GitLab and mirrors it onto a dedicated GitHub repository.

The constraint that defines the project

Nothing from the company may leave. That's a red line, not a preference. A tool that cloned the repos to replay commits would take out code belonging to the employer, which is off the table however convenient.

So the program clones nothing. It queries the GitLab API with a read-only token (read_api scope) and pulls commit metadata only. From there, it creates empty commits on the GitHub repo carrying just a date: no code, no original message, no hash, no project name. Every commit shows the same generic message.

That's the point I pushed hardest on afterwards. An early version reused the source commit's title and the internal project name in the message. Technically those aren't lines of code, but a public GitHub repo showing [acme/billing] fix VAT calculation still leaks project names and intent. The current version reproduces only the date. The graph fills in, and an observer can infer nothing from it.

How activity is reconstituted

Automatic discovery of the projects the token can reach: the tool walks the API page by page, no hand-maintained list. A new repo shows up on its own on the next sync.

Filtering by author email, to mirror only your own commits and not colleagues' on the same repos. All branches are walked, not just the main one.

Parallel fetching: commits from every project are queried at once, capped at five concurrent requests so the API isn't overwhelmed. On an organisation with dozens of repos, the difference is clear.

Preservation of original dates, without which everything would land on the sync day and the graph would mean nothing. The GitLab commit's author date becomes the GitHub commit's date.

Resuming after an interruption

Syncing hundreds of commits takes time, and a long-running script always ends up cut off at the wrong moment. The tool keeps a record of the commits already processed, identified by their hash, and saves it every fifty operations. An interrupted run picks up where it left off, without creating duplicates.

That record stays on my machine, it isn't pushed to GitHub: the hashes of company commits don't leave either. It's also what makes the tool re-runnable at will: you can launch it as many times as you like, it only handles what's new.

Built for company GitLab instances

Company instances are rarely on gitlab.com. So the instance URL is configurable, and an option accepts an internal certificate when GitLab sits behind an in-house certificate authority. Without that, the tool would be unusable in the very place it's meant for.

Meant to run on its own

A tool like this is only worth it if you can forget about it: it has to launch itself, regularly. Since it's re-runnable with no risk of duplicates, you wire it to a scheduled job and stop thinking about it.

That's exactly what I do with CronLab, my own task scheduler for Windows, which runs the sync every day and notifies me on failure. The two projects complement each other: one produces a command to run regularly, the other runs it.

What I took from it

A tool designed to be re-run harmlessly is far nicer to use than a script you only dare execute once. Resuming after an interruption and avoiding duplicates are what let you wire it to a schedule and forget about it.

The other point is a matter of restraint. The GitHub side relies on simple-git: create the local repo, place the commits at the right dates, push. What I refused was to clone the GitLab repos, and I kept what lands on GitHub to the strict minimum. Copying everything would have been simpler; sticking to one date per commit took a little more thought, and it's what makes the tool defensible to the employer.

Technologies used