node-gitlab-2-github: moving issues, milestones and wikis off GitLab
Migrate Issues, milestones etc from gitlab to github
At a glance
- What is it?
- node-gitlab-2-github is a TypeScript script that copies issues, milestones, wiki content and attachments from a GitLab instance into a GitHub repository, and it expects you to mirror the repository itself first so that commits, branches and issue references keep their autolinking. Attachments can be copied to S3 or Azure Blob Storage.
- Who is it for?
- The script suits a team that has decided to leave GitLab for GitHub and still needs its issues, milestones and wiki to come with the code, and that can supply API tokens for both services. It does not suit an organisation that wants the two platforms to stay in sync afterwards, because this is a one-shot migration with a settings file rather than a synchroniser.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 94 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What this migrates, and what it expects you to do first
The repository description is one line: migrate Issues, milestones and similar items from GitLab to GitHub. The package metadata describes it slightly differently, as migrating Issues and Wiki from GitLab to GitHub, and the keywords list issue and issues alongside import, export, GitHub and GitLab.
So the scope is the metadata layer of a project, not its code. The issues, the milestones, the wiki pages and the attachments move across, and the code itself moves by a separate, ordinary mechanism.
That separation is the first thing to understand, because the README is explicit that the repository has to be mirrored before the script is useful. You clone the GitLab repository with the --mirror option, which is like --bare but also copies all refs as-is, and you push it to GitHub with the --mirror option too.
git clone --mirror [email protected]:username/repo.git
# Change into newly created repo directory
cd repo
# Push to GitHub using the `--mirror` option. The `--no-verify` option skips any hooks.
git push --no-verify --mirror [email protected]:username/repo.gitThen you point the push URL at the mirror and keep it synchronised.
# Set push URL to the mirror location
git remote set-url --push origin [email protected]:username/repo.git
# To periodically update the repo on GitHub with what you have in GitLab
git fetch -p origin
git push --no-verify --mirrorThe README states the reason plainly: after doing this, the autolinking of issues, commits and branches will work. Issue numbers refer to commits, and a commit hash refers to a branch, and those references only resolve if the same objects exist on the GitHub side. The mirror is not a convenience, it is a prerequisite.
Install, configure, run
The installation is a clone and an npm install.
The three commands are git clone of the repository URL, a cd into node-gitlab-2-github, and npm i.
You need Node.js and npm installed, and the README says so before anything else.
Configuration is a TypeScript file, and you create it from the sample.
The copy is `cp sample_settings.ts settings.ts`.
Then you edit settings.ts and start the script.
The start command is `npm run start`.
The npm script runs the TypeScript entry point through ts-node, node node_modules/ts-node/dist/bin.js ./src/index.ts, so there is no build step to run first.
There is one identity requirement that is easy to miss. The user must be a member of the project you want to copy, and the README states that this user must be the one whose credentials are used, which is what the token_owner setting and the session cookie option are about.
The repository is a small TypeScript project with a src/ directory, tsconfig.json and tsconfig.dev.json, a .prettierrc, a .vscode directory, a Dockerfile, a Makefile and a husky pre-commit hook that runs lint-staged. The Makefile is small enough to read in one breath: it defines build-image, docker-run and docker-run-bind targets over a node-gitlab-2-github image with the latest tag.
Docker, and binding settings.ts from the host
If you would rather not install Node.js locally, the containerised route is documented, and the README notes it is for people who do not have Node.js in their local environment and do not want to install it.
You build the image and run it.
The build command is `docker build -t node-gitlab-2-github:latest .` and the run command is `docker run node-gitlab-2-github:latest`.
The Makefile offers the same two steps as build-image and docker-run, and the detached form is a single flag change.
The detached form adds the -d flag to the same run command.
The interesting variant is the bind mount, which the README describes as a way to optimise the usage of the Dockerised application. Because settings.ts is mounted from the host, changing it in your editor changes it inside the container, so you do not rebuild the image every time you correct a token.
The bind mount command starts with `docker run --mount type=bind`, with the source set to the settings.ts file in the current directory, the target set to /app/settings.ts, and readonly as the mount option.
The mount is read only, which is right: the container has no business rewriting your settings. The command must work for Linux or Mac, and the README points at the Docker documentation for Windows, where the pwd command expands incorrectly and needs different handling. The Makefile target docker-run-bind does the same thing with the local path resolved by the shell.
The image itself is worth a sentence. It is based on node:22-bullseye-slim, creates a non-root user called migrator with a uid of 2000, copies the project in with the ownership set to that user, and runs npm i as that user before starting npm run start through a bash entrypoint.
The GitLab half of the settings file
The README documents each setting in detail, and the GitLab side has a few that need judgement rather than a value.
gitlab.url is where your instance is hosted, and the default is the official http://gitlab.com domain. gitlab.token is a personal access token created under Settings and Access Tokens, with the api and read_repository scopes. gitlab.projectID is the project to copy, and it can be a string or a number.
The projectID setting has a discovery behaviour worth reading twice. Leave it null for the first run, and leave csvImport.projectMapCsv empty as well, and the script will show you which projects there are. Leave it null if you want to import multiple projects via a CSV file instead, by setting csvImport.
gitlab.listArchivedProjects controls whether archived projects are included when the script lists projects on the first run, and it defaults to true.
The most interesting setting is gitlab.sessionCookie, and the README explains the reason with a link to a GitLab issue. GitLab's API does not allow downloading attachments, and only images can be downloaded over HTTP. To work around that limitation and enable binary attachments to be migrated, you can use the session cookie set in the browser after logging in to the instance, and the cookie is named _gitlab_session.
That is a workaround with a cost. It means a live browser session credential is being handed to a script, and a session cookie expires, so this is the fragile part of the configuration.
The GitHub side, and the option that deletes your repository
The GitHub settings mirror the structure: github.baseUrl for where the instance is hosted, defaulting to the official github.com domain, and github.apiUrl pointing at the API, defaulting to api.github.com. github.owner is the organisation or user the new project will live under, and github.ownerIsOrg is a boolean, defaulting to false, that says whether that owner is an organisation.
github.token is a personal access token from Settings, Developer settings, Personal access tokens, generated with the repo scope. github.token_owner is the user name of the user whose token is used, and it is required, because it determines whether the user running the migration is also the creator of comments and issues. If that is the case and useIssueCreationAPI is true, the extra line specifying who created a comment or issue will not be added. In other words, authorship of migrated items is only preserved when the migrating user is not the original author.
github.repo is the name of the new repository, and github.recreateRepo is the one to read twice. If true, and it defaults to false, the script will try to delete the destination GitHub repository if present and recreate it. The token must be granted delete_repo scope for that to work, and the newly created repository will be made private by default.
Two constraints follow. If recreateRepo is true and the repository belongs to an organisation, ownerIsOrg must be set to true. And the README says you will always be prompted for confirmation, which is reassuring but not a substitute for reading the sentence above.
The other side of the repository is the attachment storage, which is optional. S3 can store attachments from issues, and if it is omitted a has attachment label will be added to the GitHub issue instead. The s3 settings are accessKeyId and secretAccessKey for credentials that must have write permissions on the bucket, s3.bucket for an existing bucket with an appropriate security policy, where one possible policy allows public access, and s3.region for the bucket's region. Azure Blob Storage is documented as an alternative to s3, and the README section on it is cut short in the version available here.
Where a migration script is the wrong tool
The clearest case is a team that wants both platforms to keep working. This is a one-shot copy with a settings file and a start script, not a synchroniser, and the README describes nothing that would keep the two sides aligned after the run. If your plan involves a period where engineers work in both systems, this is the wrong shape of tool.
The second case is a migration that does not need the issue history. The script's premise is that the GitHub repository already holds the code, thanks to the mirror step, and the remaining question is what to do about the issues. If the answer is start clean, a fresh repository and a hand written note to the team is simpler than a token exchange with two platforms.
The third case is a security sensitive environment. The configuration asks for a GitLab personal access token with api scope, a GitHub token with repo scope, and optionally a live GitLab browser session cookie so that binary attachments can be downloaded. The session cookie in particular is a credential that expires and that the script needs you to paste out of a browser.
The alternative worth naming is doing the copy by hand against the two APIs, which is what this project does for you. The GitLab side is reached through @gitbeaker/node and the GitHub side through @octokit/rest, with @octokit/plugin-throttling in the dependency list, so rate limiting on the GitHub API is handled rather than ignored. The difference in approach is that this project has already encoded the settings schema, the autolinking assumption, the attachment workarounds and the recreateRepo flow, while a script of your own would be smaller, exactly as configurable, and would have to rediscover all four.
Editorial conclusion
The script suits a team that has decided to leave GitLab for GitHub and still needs its issues, milestones and wiki to come with the code, and that can supply API tokens for both services. It does not suit an organisation that wants the two platforms to stay in sync afterwards, because this is a one-shot migration with a settings file rather than a synchroniser. Verify first that your mirror is in place, because the autolinking of issues, commits and branches depends on it, and read the github.recreateRepo section carefully, since that option deletes the destination repository.
Frequently asked questions
How do I install and run node-gitlab-2-github?
You need Node.js and npm, then clone the repository, run npm i, copy the sample settings with cp sample_settings.ts settings.ts, edit settings.ts, and run npm run start. The npm script launches the TypeScript entry point through ts-node, so no build step is required.
Which API tokens does the migration need?
A GitLab personal access token with the api and read_repository scopes, and a GitHub personal access token with the repo scope. If github.recreateRepo is enabled, the GitHub token also needs delete_repo, and a GitLab browser session cookie named _gitlab_session is needed to migrate binary attachments.
Do I have to mirror the GitLab repository before running the migration?
Yes. The README says you must mirror your GitLab repository to the new GitHub repository first, because after doing that the autolinking of issues, commits and branches works. The mirror is done with git clone --mirror followed by git push --no-verify --mirror.
How are issue attachments handled?
They can be copied into S3 or Azure Blob Storage, with the S3 options being accessKeyId, secretAccessKey, bucket and region, and the IAM user owning the credentials needing write permissions on the bucket. If no storage is configured, the GitHub issue gets a has attachment label instead.
Can I run the migration in Docker without installing Node.js?
Yes. Build with docker build -t node-gitlab-2-github:latest . and run with docker run node-gitlab-2-github:latest, or use the make build-image and make docker-run targets. A bind mount of settings.ts from the host keeps the configuration editable without rebuilding the image.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/piceatech-node-gitlab-2-github)Was this useful?