
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:
- GitHub-Integration with Vercel
- Update git repo origin
- Replace GitHub links in repos
- Migrate GitHub Actions
- Redirect people to Codeberg
- Archive GitHub repos
- Mirror to GitHub
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.
Replace GitHub links in repos
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 developmentTags: github , codeberg , migration
Edit this page
Show statistic for this page