# Migrate from GitLab

Codebahn imports your repositories, issues, merge requests, labels, milestones, and releases from GitLab. Code and history come across cleanly. GitLab CI does not run on Codebahn, so pipelines need a rewrite (see [CI workflows](#ci-workflows) below).

You can import through the Codebahn web UI or the [`codebahn` CLI](/docs/cli/). Both require a GitLab access token. Every repository lives inside an organization, so create one first if you have not already (see [getting started](/docs/getting-started/)).

## Prerequisites

- A Codebahn account with an organization (see [getting started](/docs/getting-started/))
- A GitLab personal access token or project access token with the **`read_api`** scope (see below)
- Git installed locally (for pointing your clone at the new remote afterward)

## Create a GitLab access token

A personal access token is the simplest option and works on all GitLab tiers. A project access token limits access to a single project but requires GitLab Premium or Ultimate on gitlab.com.

**Personal access token:** avatar > **Edit profile** > sidebar **Access** > **Personal access tokens**. Set a name and expiration, select the **`read_api`** scope, and create the token.

**Project access token** (Premium/Ultimate only): project **Settings** > **Access tokens**. Set a name and expiration, select the **`read_api`** scope, assign at least Reporter role, and create the token.

Copy the token value; it is shown once.

:::note[`read_api` vs `api`]
The migration only reads data from GitLab. The `read_api` scope is sufficient and is the least-privilege option. The full `api` scope also works if you already have a token with it. Revoke the token once the migration is done.
:::

:::caution[Fine-grained tokens]
GitLab also offers fine-grained personal access tokens with per-resource permissions. These work but require multiple scopes to cover everything the importer reads. If you use a fine-grained token, you need at minimum:

- **Metadata: Read** (under Global > System Access) for API version checks
- **Code: Read** (under Group and project > Repository) for cloning
- **Work Item: Read** (under Group and project > System Access) for milestones

A classic personal access token with `read_api` is simpler and covers all of the above in a single scope. We recommend it for migrations.
:::

### What gets imported

| GitLab | Codebahn |
|---|---|
| Repository (code, branches, tags) | Repository |
| Issues | Issues |
| Merge requests | Pull requests |
| Labels | Labels |
| Milestones | Milestones |
| Releases | Releases |

Merge requests import as pull requests, with content, comments, and state preserved.

GitLab group membership and project permissions do not transfer. After import, [invite your team](/docs/getting-started/) and set repository access on the Codebahn side.

## Import via the web UI

1. Log in to Codebahn

2. Click **+** > **Import repository** > **GitLab**

3. In **Clone URL**, paste the HTTPS URL of your repository (for example `https://gitlab.com/your-group/your-project.git`, from the project page under **Code** > **Clone with HTTPS**)

4. Paste your access token in the **Access token** field

5. Select what to import: issues, merge requests, labels, milestones, releases

6. Choose your organization as the owner and confirm the repository name

7. Click **Migrate repository**

This is a one-time import. Once it completes, the Codebahn repository is independent from GitLab.

Point your local clone at the new remote:

```bash
git remote set-url origin git@codebahn.net:your-org/your-repo.git
```

## Import with the CLI

The [`codebahn` CLI](/docs/cli/) can import from GitLab. [Install the CLI](/docs/cli/#install) first. Pass the GitLab access token with `--source-token`.

### Single project

```bash
codebahn repo import gitlab.com/your-group/your-project --source-token $GITLAB_TOKEN
```

By default, the CLI imports issues, merge requests (as pull requests), labels, milestones, and releases. To skip specific metadata:

```bash
codebahn repo import gitlab.com/your-group/your-project --source-token $GITLAB_TOKEN --no-issues --no-prs
```

### Entire group

```bash
codebahn repo import gitlab.com/your-group --owner my-codebahn-org --source-token $GITLAB_TOKEN -y
```

Use `--include` and `--exclude` with glob patterns to select projects from the group.

## CI workflows

:::caution[GitLab CI does not run on Codebahn]
Codebahn runs GitHub Actions-compatible workflows. Your `.gitlab-ci.yml` files are ignored. Rewrite them as Actions workflows in `.github/workflows/`.
:::

The two systems differ in syntax, structure, and runner model. A starting map:

| GitLab CI | Actions |
|---|---|
| `.gitlab-ci.yml` | `.github/workflows/*.yml` |
| `stages` | Jobs run in parallel; use `needs` for ordering |
| `script` | `steps` with `run` |
| `image` | `container` or runner label (`runs-on`) |
| `only/except` or `rules` | `on` triggers with path/branch filters |
| `artifacts` | `actions/upload-artifact` / `actions/download-artifact` |
| `cache` | `actions/cache` |
| `variables` | `env` at workflow, job, or step level |

There is no automated converter. Port one pipeline at a time, starting with the simplest (tests on push), and verify each before moving on. See [set up CI](/docs/set-up-ci/) for a working example and [CI runners](/docs/ci-runners/#compatibility) for runner sizes and what is supported. Hosted runners use the labels `ubuntu-latest` (alias for `codebahn-small`), `codebahn-medium`, and `codebahn-large`.

Codebahn also reads workflows from `.codebahn/workflows/`, `.forgejo/workflows/`, and `.gitea/workflows/`. See [workflow directories](/docs/workflow-directories/) for the precedence order.

## SSH keys

Add your SSH keys under **Settings > SSH / GPG Keys**, the same as on GitLab.

## Troubleshooting

**"Authentication failed" or 401**
The token is expired or missing the `read_api` scope. Generate a new one.

**403 or "insufficient_granular_scope"**
You are using a fine-grained personal access token that is missing required permissions. The error message names the missing scope (e.g. `Metadata: Read`, `Code: Download`, `Work Item: Read`). Either add the listed permission to the token, or switch to a classic personal access token with the `read_api` scope, which covers everything the importer needs.

**"Not found" or 404**
The token cannot see this project. For personal tokens, confirm your GitLab account has at least Reporter membership on the project. For project tokens, confirm the token was created with at least Reporter role.

**Merge requests imported without comments or review threads**
GitLab may rate-limit the importer on large projects. If the migration fails or returns incomplete data, retry later or migrate during off-peak hours on gitlab.com.

## Fallback: manual push

If the migration tool does not fit your situation, clone and push by hand:

```bash
git clone --bare https://gitlab.com/your-group/your-project.git
cd your-project.git
git push --mirror git@codebahn.net:your-org/your-repo.git
```

This moves code and branches only, not issues, merge requests, or releases.

## Migrating a group

**CLI:** import an entire group in one command. See [Import with the CLI](#import-with-the-cli) above.

**Web UI:** run one migration per project with the org as the owner.

**API:** script it against the [migration API](https://forgejo.org/docs/latest/developer/api-usage/).

## Leaving later

Portability cuts both ways. Whenever you want a full copy of everything (every repository plus issues, pull requests, labels, milestones, and releases), open [Org Settings > Export](/docs/export/) and download a single archive. One click to start; the archive builds in the background and is available for 24 hours.
