Janik von Rotz


8 min read

Migrate from Github to Codeberg

Since the enshittification of GitHub I decided to become a Berger instead of Hubber. Which I means that I wanted to move all my repos from github.com to codeberg.org.

Running a migration script is easy. But of course there are many details to consider once the repos have been moved. In this post I’ll brief you on my experience and give you details on these challenges:

Running the migration

As mentioned running a migration script that copies the repos from GitHub to Codeberg is easy.

The heavy work was done by https://github.com/LionyxML/migrate-github-to-codeberg. In order to run this script you need to create a token with read/write access to user, organisation and repo for Codeberg. Create the token here: https://codeberg.org/user/settings/applications Then you do the same for GitHub. Create a token with read/write access to user and repo here: https://github.com/settings/tokens

Clone the miggation script and update the variables. Run the script ./migrate_github_to_codeberg.sh and you should get an output like this:

>>> Migrating: Bundesverfassung (public)...
 Success!
>>> Migrating: Hack4SocialGood (public)...
 Success!
>>> Migrating: Quarto (public)...
 Success!
>>> Migrating: WebPrototype (public)...
 Success!
>>> Migrating: Website (public)...
 Success!
>>> Migrating: raspi-and-friends (public)...
 Success!

One issue was that the script migrated all repos from all of connected organisations. I had to delete all repos in Codeberg. The following script helped doing so:

#!/bin/bash

CODEBERG_USERNAME="janikvonrotz"
CODEBERG_TOKEN="*******"

repos_response=$(curl -s -f -X GET \
  https://codeberg.org/api/v1/users/$CODEBERG_USERNAME/repos \
  -H "Authorization: token $CODEBERG_TOKEN")

if [ $? -eq 0 ]; then
  repo_names=($(echo "$repos_response" | jq -r '.[] |.name'))

  for repo_name in "${repo_names[@]}"; do
    echo "Deleting repository $repo_name..."

    delete_response=$(curl -s -f -w "%{http_code}" -X DELETE \
      https://codeberg.org/api/v1/repos/$CODEBERG_USERNAME/$repo_name \
      -H "Authorization: token $CODEBERG_TOKEN")

    if [ $delete_response -eq 204 ]; then
      echo "Repository $repo_name deleted successfully."
    else
      echo "Failed to delete repository $repo_name. Status code: $delete_response"
    fi
  done
else
  echo "Failed to retrieve repository list. Status code: $?"
fi

I had to run the script multiple times because of the API paging.

To ensure the migration is done for repos that are assigned tomy account, I had to set owners variable in the script:

OWNERS=(
    "janikvonrotz"
)

And with another run ./migrate_github_to_codeberg.sh the script copied all repos from GitHub to Codeberg.

GitHub-Integration with Vercel

I use Vercel to build and publish my static website. If you use Netlify you probabley face the same problem. Vercel is tightly integrated with GitHub. At the time of writing this post there was no integration for Codeberg available. So it was either stick with GitHub or get rid of the integartion.

I decided to get rid of it and uninstall the Vercel app on GitHub. You can access your GitHub apps here: https://github.com/settings/installations

This will cause the Vercel projects to be disconnected from the GitHub project and thus they will no longer be deployed automatically.

To deploy the websites you can use the Vercel cli. It is simple as cake once you are looged in. Here is an example of such a deployment:


[main][~/taskfile.build]$ vercel --prod
Vercel CLI 44.7.3
🔍  Inspect: https://vercel.com/janik-vonrotz/taskfile-build/4zoKQnE7osV9udRnUyBYdX9EzNif [3s]
✅  Production: https://taskfile-build-5vtumwoja-janik-vonrotz.vercel.app [3s]
2025-08-20T11:20:17.987Z  Running build in Washington, D.C., USA (East) – iad1
2025-08-20T11:20:17.988Z  Build machine configuration: 2 cores, 8 GB
2025-08-20T11:20:18.006Z  Retrieving list of deployment files...
2025-08-20T11:20:18.518Z  Downloading 54 deployment files...
2025-08-20T11:20:19.233Z  Restored build cache from previous deployment (BuULWL9zESMSfPa8QYN5gyoGA4JP)
2025-08-20T11:20:21.264Z  Running "vercel build"
2025-08-20T11:20:21.727Z  Vercel CLI 46.0.2
2025-08-20T11:20:22.364Z  Detected `pnpm-lock.yaml` 9 which may be generated by pnpm@9.x or pnpm@10.x
2025-08-20T11:20:22.365Z  Using pnpm@9.x based on project creation date
2025-08-20T11:20:22.365Z  To use pnpm@10.x, manually opt in using corepack (https://vercel.com/docs/deployments/configure-a-build#corepack)
2025-08-20T11:20:22.380Z  Installing dependencies...
2025-08-20T11:20:23.109Z  Lockfile is up to date, resolution step is skipped
2025-08-20T11:20:23.148Z  Already up to date
2025-08-20T11:20:23.992Z
2025-08-20T11:20:24.001Z  Done in 1.4s using pnpm v9.15.9
2025-08-20T11:20:26.202Z  [11ty] Writing ./_site/index.html from ./README.md (liquid)
2025-08-20T11:20:26.208Z  [11ty] Benchmark     73ms  19%     1× (Configuration) "@11ty/eleventy/html-transformer" Transform
2025-08-20T11:20:26.208Z  [11ty] Copied 4 Wrote 1 file in 0.38 seconds (v3.0.0)
2025-08-20T11:20:26.303Z  Build Completed in /vercel/output [4s]
2025-08-20T11:20:26.399Z  Deploying outputs...

Of course it is possible to setup a CI job that installs the Vercel cli and runs the prod deployment.

Update git repo origin

For all the local git repos you need to update the remote. The local remote url will still point to github.com and needs to replaced with the coderberg.org url. The following script finds git repos in the home folder and upates the matching url:

#!/bin/bash

OLD_URL="git@github.com:janikvonrotz/"
NEW_URL="git@codeberg.org:janikvonrotz/"

for REPO in $(find "$HOME" -maxdepth 2 -type d -name '.git'); do
    DIR=$(dirname "$REPO")
    cd "$DIR"
    CURRENT_URL=$(git config --get remote.origin.url)
	NEW_CURRENT_URL=$(echo "$CURRENT_URL" | sed "s|$OLD_URL|$NEW_URL|")
    if [ "$NEW_CURRENT_URL" != "$CURRENT_URL" ]; then
        git remote set-url origin "$NEW_CURRENT_URL"
        echo "Updated origin URL for $(basename "$(pwd)") to: $NEW_CURRENT_URL"
    fi
done

Submodule links in the .gitmodules have to be updated manually.

Not only the git remote links to github.com, but also the content stored in the repo. I often add a git clone command to the usage section in the README.md. The clone url has to be updated.

I was able to solve this issue with semi-automated approach. I created several search and replace commands that look for github.com link patterns. The search pattern considers external links to github.com that had to be preserved.

On the command line I entered the repo and ran the replacement commands:

# 1. Fix github.com/blob → codeberg.org/src/branch
rg 'github\.com(:|/)(janikvonrotz)/[^/]+/blob/(main|master)' -l | \
  xargs sed -i 's|github\.com\(:\|/\)\(janikvonrotz\)/\([^/]\+\)/blob/\(main\|master\)\(/[^"]*\)\?|codeberg.org/\2/\3/src/branch/\4\5|g'

# 2. Fix github.com/tree → codeberg.org/src/branch
rg 'github\.com(:|/)(janikvonrotz)/[^/]+/tree/(main|master)' -l | \
  xargs sed -i 's|github\.com\(:\|/\)\(janikvonrotz\)/\([^/]\+\)/tree/\(main\|master\)\(/[^"]*\)\?|codeberg.org/\2/\3/src/branch/\4\5|g'

# 3. Fix raw.githubusercontent.com → codeberg.org/raw/branch
rg 'raw\.githubusercontent\.com/(janikvonrotz)/[^/]+/(main|master)' -l | \
  xargs sed -i 's|raw\.githubusercontent\.com/\(janikvonrotz\)/\([^/]\+\)/\(main\|master\)\(/[^"]*\)\?|codeberg.org/\1/\2/raw/branch/\3\4|g'

# 4. Fix bare repo URLs: github.com/user/repo → codeberg.org/user/repo
rg 'github\.com(:|/)janikvonrotz/[^/"?#]+' -l | \
  xargs sed -i 's|github\.com\(:\|/\)\(janikvonrotz\)/\([^/"?#]\+\)|codeberg.org/\2/\3|g'

# 5. Fix user profile URLs
rg 'https://github\.com/janikvonrotz\b' -l | \
  xargs sed -i 's|https://github\.com/janikvonrotz|https://codeberg.org/janikvonrotz|g'

rg 'https://github\.com/jankvonrotz\b' -l | \
  xargs sed -i 's|https://github\.com/jankvonrotz|https://codeberg.org/jankvonrotz|g'

In some cases simply replacing a link was not possible. For example Vuepress linked by default to GitHub and I had to change the .vuepress/config.js manually:

repo: 'https://codeberg.org/janikvonrotz/$REPO',
repoLabel: 'Codeberg',
docsBranch: 'main',

Nonetheless replacing the links was easier than expected.

Migrate GitHub Actions

For my personal repos I didn’t run a a lot of GitHub Actions. One of the few was this action: https://github.com/janikvonrotz/janikvonrotz.ch/blob/main/.github/workflows/build.yml
It builds and pushes a Docker image to Docker registry.

Codeberg offers two ways to run jobs. There is the Woodpecker CI: https://docs.codeberg.org/ci/#using-codeberg's-instance-of-woodpecker-ci
And there are Forgejo Actions: https://docs.codeberg.org/ci/actions/#installing-forgejo-runner

I decided to use Forgejo Action. First I enabled Forgejo Actions in the repos settings. Next I created the DOCKER_PAT secret in the user settings: https://codeberg.org/user/settings/actions/secrets.

Forgejo Actions support the same YAML spec and thus I only need to rename the .github folder to .forgejo. I pushed the changes and the first run was created: https://codeberg.org/janikvonrotz/janikvonrotz.ch/actions/runs/1

However the was waiting for the default Forgejo runner and it seemed not be meant for public use. So I decided to provide my own Forgejo runner.

I created a Helm chart to deploy a Forgejo runner: https://kubernetes.build/forgejoRunner/README.html
Further I updated the .forgejo/workflow/build.yml to use the provided runner. The setup worked but it turned out that most of the CI dependencies are not in the YAML but on the runner. As I understand GitHub Action runners are actual virtual machines on Azure. Replicating these environments is not possible. Also building a multi-platform Docker image with Docker in Docker inside a Kubernetes cluster is not the best idea.

I decided to put this issue on hold. As an alternative I setup a mirror from the Codeberg repo to GitHub (see section below).

Redirect people to Codeberg

It is not possible to redirect repo visitors automatically from GithHub to Codeberg. I decided to update the repo description with a link to the new location. The following script walks through the GitHub repos and updates the description:

CODEBERG_URL="https://codeberg.org/janikvonrotz/"
GITHUB_USERNAME="janikvonrotz"
GITHUB_TOKEN="*******"

GITHUB_PAGINATION=100
github_total_repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" | jq '.public_repos +.total_private_repos')
github_needed_pages=$(( ($github_total_repos + $GITHUB_PAGINATION - 1) / $GITHUB_PAGINATION ))

for ((github_page_counter = 1; github_page_counter <= github_needed_pages; github_page_counter++)); do
    repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/repos?per_page=${GITHUB_PAGINATION}&page=${github_page_counter}")
    for repo in $(echo "$repos" | jq -r '.[] | select(.owner.login == "'"$GITHUB_USERNAME"'") |.name'); do
        echo "Update repo description for $GITHUB_USERNAME/$repo:"
        new_description="This repository has been moved to $CODEBERG_URL$repo. Please visit the new location for the latest updates."
        curl -X PATCH \
            -H "Authorization: token $GITHUB_TOKEN" \
            -H "Content-Type: application/json" \
            https://api.github.com/repos/$GITHUB_USERNAME/$repo \
            -d "{\"description\":\"$new_description\"}"
    done
done

Archive GitHub repos

Archiving a repo on GitHub means that it is no longer maintained there. Also the archived repo becomes readonly. With the following script I archived all my GitHub repos:

#!/bin/bash

ARCHIVE_MESSAGE="Repository migrated to Codeberg."
GITHUB_USERNAME="janikvonrotz"
GITHUB_TOKEN="*******"

GITHUB_PAGINATION=100
github_total_repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" | jq '.public_repos +.total_private_repos')
github_needed_pages=$(( ($github_total_repos + $GITHUB_PAGINATION - 1) / $GITHUB_PAGINATION ))

for ((github_page_counter = 1; github_page_counter <= github_needed_pages; github_page_counter++)); do
    repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/repos?per_page=${GITHUB_PAGINATION}&page=${github_page_counter}")
    for repo in $(echo "$repos" | jq -r '.[] | select(.owner.login == "'"$GITHUB_USERNAME"'") |.name'); do
        echo "Archive repo $GITHUB_USERNAME/$repo:"
        curl -X PATCH -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" https://api.github.com/repos/$GITHUB_USERNAME/$repo -d "{\"archived\":true,\"archive_message\":\"$ARCHIVE_MESSAGE\"}"
    done
done

Mirror to GitHub

Mirroring a repo to GitHub solved the problem I had with running the GitHub Actions in the Codeberg environment. It is possible to mirror a Codeberg repo to GitHub and thus you can trigger GitHub Actions with the push of commit.

In the mirror settings of your repo, in my case it was https://codeberg.org/janikvonrotz/janikvonrotz.ch/settings, you can setup a push url. Enter the same credentials as used in the migration script and ensure to tick the push on commit box.

Summary and outlook

Not being able to run my own Forgejo runner was very frustrating. I think CI should not be that hard. I will try to setup a Forgejo runner on a bare metal vm and build my website image with it.

Overall moving my personal repos from GitHub to Codeberg was easy. I did not consider to move the repos for my organisation yet. I think this will be a much more difficult challenge. The organisation repos are integrated deeply into many other projects. The best approach I can think of is mirroring the repos from GitHub to Codeberg and start the transition with one repo and move a long the linked repos.

Categories: Software development
Tags: github , codeberg , migration
Edit this page
Show statistic for this page