Migrating from GitHub to Codeberg (Forgejo/Gitea)

I was always a fan of diversity in open source software and hosted offerings. It’s partially why I run my own e-mail server. With the advent of using other people’s code to train your own AI models and deteriotating relations across the Atlantic, I have decided to foster diversity by moving my code off of GitHub.

My new home for private projects is Codeberg, a Forgejo instance (which is a fork of Gitea) hosted by a non-profit organization in Berlin, Germany. It brings all the advantages of Gitea (i.e. relatively broad support in tools like goreleaser) with the benefit of adhering to German law. Most of what I describe here is therefore applicable to moving to any Gitea instance.

Thankfully, kind of, I have no big following on GitHub and there are basically no issues or pull requests in any of my repositories. If there were, Codeberg could import them, but I would surely lose a lot of previous contributors due to the friction of having “yet another platform”.

What is there to migrate?

I have mostly open source Go tools on GitHub, so my migration will have to focus on the following aspects:

Table of Contents

  1. Moving the code
  2. Update your code
  3. Update your vanity Go module URLs
  4. goreleaser
  5. GitHub Actions
    1. Running your own CI Runner
    2. Enabling Docker Builds
  6. Finishing up

1) Moving the code

This is the easy part. Just sign up on your favorite Gitea instance and create a new repository. Choose to migrate your existing GitHub repository:

Codeberg screenshot showing the migrate form

I personally did not bother to input a token and import issues/PRs.

2) Update your code

You probably have some referenfes in your README file regarding GitHub. Now is a good time to update those and also to update your git remotes in your local working copy, so origin now points to Codeberg.

git remote set-url origin git@codeberg.org:owner/project.git
git remote add github git@github.com:owner/project.git
git fetch github

Now you should be able to, during the migration, keep Codeberg and GitHub separate.

You can now set a new Git tag in your repo, which will be helpful in testing the changes from the next section of this guide.

3) Update your vanity Go module URLs

I use go.xrstf.de as the vanity domain for all my Go projects, and vangen to create the necessary HTML pages. To move a repository from GitHub to Codeberg, a little bit more configuration is necessary since my vangen fork doesn’t (yet) support Codeberg directly.

repositories:
  # old (GitHub) style configuration
  #- prefix: stalk
  #  main: true
  #  url: https://github.com/xrstf/stalk

  # change it to a more explicit configuration for Codeberg
  - prefix: stalk
    main: true
    type: git
    url: https://codeberg.org/xrstf/stalk
    source:
      home: "https://codeberg.org/xrstf/stalk"
      dir: "https://codeberg.org/xrstf/stalk/tree/main{/dir}"
      file: "https://codeberg.org/xrstf/stalk/blob/main{/dir}/{file}#L{line}"

Run vangen to re-generate your public/ folder.

Since Go/Google’s GOSUMDB will have probably cached the URLs for all existing tags in your repository, you might need to tag a new version (potentially even on the same commit as your last version) on Codeberg so you can force Go to re-find your code.

Important: Before you now go ahead and try to go get your code from your new location, absolutely make sure that the metatags in your new HTML files are correct. They should look like this:

<meta name="go-import" content="go.xrstf.de/stalk git https://codeberg.org/xrstf/stalk">
<meta name="go-source" content="go.xrstf.de/stalk https://codeberg.org/xrstf/stalk https://codeberg.org/xrstf/stalk/tree/main{/dir} https://codeberg.org/xrstf/stalk/blob/main{/dir}/{file}#L{line}">

If this is invalid, fix it before trying to use it. Go’s (or better Google’s) GOSUMDB will cache anything and everything immediately and forever. If your HTML is broken and you do go get go.example.com/mytool@v1.2.3, Google will forever cache the broken configuration and version 1.2.3 is now effectively burned.

4) goreleaser

I use goreleaser to build and upload binaries whenever I set a Git tag in my repositories. Thankfully, since everything is Gitea under the hood, very few changes are necessary to switch to Codeberg.

In your .goreleaser.yaml, simply add

gitea_urls:
  api: https://codeberg.org/api/v1
  download: https://codeberg.org

You now also need to convince goreleaser to switch to Gitea, because if you use GitHub Actions (or even, as we will see further down, Forgejo Actions), there will always be a predefined GITHUB_TOKEN environment variable. To switch to Gitea mode, define a GITEA_TOKEN and then tell goreleaser to use it. In a workflow file (.github/workflows/ or .forgejo/workflows), this could look like this:

jobs:
  goreleaser:
    steps:
      # ...
      # checkout, setup, yadda yadda yadda
      # ...

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          version: '~> v2'
          args: release --clean
        env:
          # These two variables flip goreleaser into Gitea mode.
          GORELEASER_FORCE_TOKEN: gitea
          GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}

That’s it already! :-)

5) GitHub Actions

Microsoft is rich and can provide free CI resources to the world. Codeberg is not and so CI functionality is not granted to every user by default. If you want to use Forgejo Actions on Codeberg, you will have to apply for it. Even if you want to bring your own runner, you need to be approved for the features to even be enabled in your account.

Head over to https://codeberg.org/Codeberg-e.V./requests and create a new ticket, then follow the ticket template.

For me the approval process took a few hours only.

Once you are approved, you’re not quite there yet. You will need to enable the actions for each repository that you want to use them in. Head to the repository settings page and click on “Units” > “Overview” in the menu.

Codeberg screenshot showing the repository settings with checkboxes for packages, releases and actions

Enable the checkboxes for releases and actions, and if you plan on hosting packages like container images, enable that checkbox, too.

Now it’s time to migrate your GitHub workflows to Forgejo workflows. Thankfully, even though Forgejo very clearly states:

The syntax and semantics of the workflow files will be familiar to people used to GitHub Actions but they are not and will never be identical.

for me and my simple workflows, I barely had to make any changes.

Simply mv .github .forgejo and then edit your workflows. The only change I had to make was to use full URLs for most of the actions. On GitHub, where the actions originate, you can just refer to them using goreleaser/goreleaser-action@v6. This only works for some actions (those mirrored into Forgejo, like actions/checkout and actions/setup-go), but for all others simply prepend https://github.com/:

  - name: Run GoReleaser
    uses: https://github.com/goreleaser/goreleaser-action@v6
    with:
      version: '~> v2'
      args: release --clean
    env:
      GORELEASER_FORCE_TOKEN: gitea
      GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  - name: Run golangci-lint
    uses: https://github.com/golangci/golangci-lint-action@v6
    with:
      version: latest

That’s it. Commit, push, and (depending on your workflows) already observe how they work. You might need to adjust the runs-on value if you intend to use the public Codeberg infrastructure. You can see a list of runners and their labels in your Codeberg settings.

5.1) Running your own CI Runner

If you have a server and some capacity to spare, you can also run your own runner for your projects on Codeberg (it’s not public for everyone, it will only work for your own repositories).

The Forgejo Runner is open source (duh) and can be run in a variety of ways. I chose docker-compose and setup was quite straight forward.

Note: I tried to follow the register container in the docker-compose example, but was confused by the shared secret, which only ever makes sense if you also run your own Forgejo. Instead, I followed the Codeberg documentation and ran the appropriate commands manually.

My compose.yaml for the runner on my server is:

version: "3.8"

# based on https://code.forgejo.org/forgejo/runner/src/branch/main/examples/docker-compose/compose-forgejo-and-runner.yml
services:
  docker:
    image: code.forgejo.org/oci/docker:dind
    restart: unless-stopped
    hostname: docker
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
      DOCKER_HOST: docker
    volumes:
      - docker_certs:/certs
    networks:
      - runner
    logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "10"

  runner:
    image: code.forgejo.org/forgejo/runner:6.2.2
    restart: unless-stopped
    command:
      - forgejo-runner
      - --config
      - /etc/forgejo/config.yaml
      - daemon
    networks:
      - runner
    environment:
      DOCKER_HOST: tcp://docker:2376
      DOCKER_CERT_PATH: /certs/client
      DOCKER_TLS_VERIFY: "1"
    volumes:
      - ./config:/etc/forgejo
      - /srv/forgejo-runner-data:/data
      - docker_certs:/certs
    logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "10"

  # https://docs.codeberg.org/ci/actions/
  runner-register:
    image: code.forgejo.org/forgejo/runner:6.2.2
    networks:
      - runner
    environment:
      DOCKER_HOST: tcp://docker:2376
    volumes:
      - /var/forgejo-runner-data:/data
    user: 0:0
    # Here I just fiddled with the commands and re-ran the container a few times
    # to get the necessary .runner and config.yaml files.
    command: >-
      bash -ec '
      #forgejo-runner register --no-interactive --token "YOUR_CODEBERG_REGISTRATION_TOKEN_HERE" --name YOUR_RUNNER_NAME --instance https://codeberg.org;
      forgejo-runner generate-config;
      cat .runner
      '      

networks:
  runner: ~

volumes:
  docker_certs: ~

Make sure you chown the home directory for the runner (/var/forgejo-runner-data in my case) so that the runner can create files in it.

Once you ran the commands above, your runner should already show up on Codeberg:

Codeberg screenshot showing the list of CI runners

5.2) Enabling Docker Builds

The configuration above works fine for building Go, but when you want to build Docker images, you need to make sure the steps in your Forgejo Actions talk to the docker-in-docker daemon.

To achieve this, we need to mount the Docker PKI into each action container. Edit the config.yaml from your Forgejo Runner and make the following changes:

# This snippet only shows the changes that need to be made, not the entire config.yaml.

runner:
  # Set environment variables to point any docker command inside an action step
  # to the docker-in-docker daemon.
  envs:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS: yesplease
    DOCKER_CERT_PATH: /certs/client

container:
  # Make action containers use the dind host network.
  network: host

  # mount the Docker PKI
  options: "--volume /certs:/certs"

  # allow the mount above
  allowed_volumes:
    - /certs

Once the configuration is updated, restart the runner.

To now use Docker in your actions, add these steps to your workflow:

jobs:
  example:
    # enable permissions
    permissions:
      packages: write
      contents: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Docker CLI
        run: apt-get update && apt-get -qy install docker.io
        env:
          DEBIAN_FRONTEND: noninteractive

      - name: Docker Login
        uses: docker/login-action@v3
        with:
          registry: codeberg.org
          username: ${{ github.actor }}
          # The default token generated by Forgejo will not grant write permissions
          # for package repositories; you need to manually create a token in your
          # user settings, give it read&write permissions on package&repository
          # (in Forgejo: User Settings > Applications).
          #
          # Then create a new Runner secret (User Settings > Actions > Secrets)
          # and store the newly created token there, named for example "REGISTRY_TOKEN".
          password: ${{ secrets.REGISTRY_TOKEN }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Context for Buildx
        run: |
          docker context create builders          

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          endpoint: builders

      # From here on, steps can use Docker buildx to build multiarch container images.

Many thanks to Dan who helped me figure all this out :-)

7) Finishing up

You can now archive the original repository on GitHub (maybe even make one last extra commit in it to inform users about the migration to Codeberg) and then you’re already done!