GitHub → Codeberg: my experience

Published . Estimated reading time: 11 minutes.


In which I talk about the process involved in switching forges, and how well that went.

Spoiler alert: this very site that you’re reading this on is not served from GitHub Pages anymore! At this point, I’d call my migration successful. But it took more than clicking a single button, so let’s talk about the steps involved, at least for me. I’m hoping that it can help be an example for other people, and show that it’s actually not that complicated.

(My) migration process

First, I took an hour or so to set up my profile picture, email address(es), SSH keys…

Step 1: migrating the repos

This wasn’t difficult, because Forgejo (the forge software that powers Codeberg) offers a “migrate from GitHub” functionality. You need to generate a PAT on GitHub to import things like issues (which is awesome!), and as a bonus it also speeds up the process.

It was, however, tedious, because the process was entirely manual (perhaps there’s a way to automate it, like by using some Forgejo CLI tool, but I didn’t bother looking into that). And, due to GitHub API rate limits, whenever I tried importing two repos at the same time, one or both would fail. (It wasn’t too bad, though, since I could fill out the migration page for the next while one was in progress; and generally, it took me roughly as long to fill it out as it took Codeberg to perform the import.)

I’m really happy that issues, PRs, wikis, and releases can be imported flawlessly: this makes it possible to not have to refer to GitHub anymore!

Of course I don’t control all links that point to my stuff, but I could at least run rg -F github.com/ISSOtm in my home directory, to catch those within my own repos. It’s possible to automate the replacing process:

$ sed --in-place --regexp-extended 's,github.com/ISSOtm,codeberg.org/ISSOtm,' <files...>

…and if you’re feeling like bulk-replacing all files in a directory:

$ find <directory> -type f -exec sed -Ei 's,github.com/ISSOtm,codeberg.org/ISSOtm' {} +

Repositories, however, may still be pointing to GitHub:

$ git remote -v
origin	git@github.com:ISSOtm/rsgbds.git (fetch)
origin	git@github.com:ISSOtm/rsgbds.git (push)

You can either manually git remote set-url origin git@codeberg.org:ISSOtm/rsgbds.git (or the equivalent if you’re using HTTPS), or use one of the replace commands above, since remote URLs are stored textually:

# Within a single repo:
$ find .git -name config -exec sed -Ei 's,github.com:ISSOtm,codeberg.org:ISSOtm,' {} + # Replace the colons with slashes if you're using HTTPS!

# For all repos within the current directory: (requires `shopt -s globstar` if using Bash)
$ find **/.git -name config -exec sed -Ei 's,github.com:ISSOtm,codeberg.org:ISSOtm,' {} + # Ditto the above.

…then it’s a matter of pushing the changes to all of the repos.

Step 3: stubbing out the GitHub repos

I also wanted to make it clear that my repos were now living on Codeberg; so, I created a little script in an empty directory:

#!/bin/bash
set -euo pipefail

git remote set-url origin git@github.com:ISSOtm/$1
cat <<EOF >README.md
# Moved to https://codeberg.org/ISSOtm/$1

[See my blog](http://eldred.fr/blog/codeberg) as to why.
EOF
git add README.md
git commit --amend --message 'Add move notice'
git push --force
gh repo edit --description "Moved to https://codeberg.org/ISSOtm/$1" --homepage "https://codeberg.org/ISSOtm/$1"
gh repo archive --yes

Then, to run it:

$ chmod +x stub_out.sh
$ git init
$ git remote add origin ''
$ ./stub_out.sh rsgbds
$ ./stub_out.sh fortISSimO
# ...etc.

The automation made it not painful, so this went pretty well.

Step 4: porting CI

Now, onto the harder stuff :)

The first interesting thing that I noticed is this section of Codeberg’s CI documentation:

Running CI/CD pipelines can use significant amounts of energy. As much as it is tempting to have green checkmarks everywhere, running the jobs costs real money and has environmental costs.

Unlike other giant platforms, we do not encourage you to write “heavy” pipelines and charge you for the cost later. We expect you to carefully consider the costs and benefits from your pipelines and reduce CI/CD usage to a minimum amount necessary to guarantee consistent quality for your projects.

That got me to think about which projects of mine really need CI, and ultimately, I decided that I would only need CI for publishing my website, and the documentation of gb-starter-kit and fortISSimO; the rest of my projects don’t get contributions anyway, so I can live without CI on them, at least for now.

Anyway, Codeberg actually has two different CI solutions: Woodpecker, and Forgejo Actions; the former seems to be more powerful, but you need to apply for access, and the latter is very close to GitHub Actions, which should facilitate the migration. So I picked Forgejo Actions, even though it’s marked as being in beta.

It’s not very difficult to port a YAML file from GHA to Forgejo Actions; for example, look at the commit porting gb-starter-kit’s publishing CI. (This doesn’t really appear as a diff, since I’ve moved the file; but it’s small, so it’s easy to compare manually.)

Here are some salient points:

I actually spent some extra time trying to use less compute to perform my CI jobs, somewhat motivated by the small size of the runners, and because I’m guessing that the smaller the runner you’re picking, the faster your job will be able to be scheduled. Here is one such commit; note in particular line 50, where I tried1 using a Docker image with LaTeX preinstalled, which saves the time taken by apt install and requires fewer writes to the filesystem, freeing up RAM.

1

Unfortunately, due to a version discrepancy with noweb, I had to revert to the base Ubuntu image; but a “regular” LaTeX workflow would have had no problem.

Step 5: re-hosting my website

All of the previous steps were done within the span of a few days; however, since my website (this very website) was hosted using GitHub Pages, I couldn’t migrate its repos (yes, plural: you can configure individual repos to be published separately, which is how e.g. https://eldred.fr/fortISSimO is published, despite not being in the website’s main repo).

Nominally, Codeberg has an equivalent, Codeberg Pages; however, as mentioned on that page, the software behind this feature is currently in maintenance mode, because of complexity and performance issues2. So I left it at that for roughly a month, hoping there’ll eventually be an update. Also, subprojects are published as subdomains instead of subdirectories, which would have broken links (e.g. http://eldred.fr/fortISSimO would have become http://fortISSimO.eldred.fr). Meh…

And then (by chance lol) I discovered git-pages and its public instance Grebedoc3! It functions much like GitHub Pages, though with a bit more setup since it’s not integrated within the forge itself.

git-pages actually has several niceties:

Oh, and also, Codeberg’s November 2025 newsletter mentions that Codeberg is planning to gradually migrate to [git-pages]. Exciting!

I’m actually much happier using this than GitHub Pages; so, I’ve joined Catherine’s Patreon, because I want to see this go far.

2

To quote Catherine’s motivation for creating git-pages: I started out wanting to just use Codeberg Pages and then I found out that Codeberg Pages is in maintenance mode and has such poor architecture that not only does it have rather low uptime but it also regularly crashes Codeberg’s Forgejo instance itself.

3

🦃. It is intensely looking at you…….

4

Here is some context as to what this means.

Time tracking

Steps 1 through 3 (migrating the repos) took me the better part of an afternoon; step 4 (porting CI) took me another afternoon, mostly to learn the new CI system; and step 5 (the website) took me… well, it should have taken an afternoon, but I used the opportunity to also pay down some tech debt (merging my slides repo into my main website), which took a few days due to required rearchitecting.

All in all, even with 45 repos migrated, this basically took a weekend. And I didn’t find it annoying!

Since the task seemed really daunting, my anxiety caused me to procrastinate this a lot, but in the end it was little work. One of the reasons I’m writing this is to let other people know that, so they can overcome their own anxiety. Maybe. :P

What now?

All in all, I’m very happy with this migration! As far as I can tell, nothing on this website has broken, and I’ve tried reasonably containing the breakage over on GitHub: I have truncated the master branches, but all other branches and tags remain in place (mostly due to laziness lol), permalinks (e.g. https://github.com/ISSOtm/gb-bootroms/blob/c8ed9e106e0ab1193a57071820e46358006c79d0/src/dmg.asm) still work, only non-perma links (e.g. https://github.com/ISSOtm/gb-bootroms/blob/master/src/dmg.asm) are broken, but those are unreliable in the first place anyway.

Since that means that all of my code is still on GitHub, I want to delete my repos; but that would be a bad idea at this point, due to leaving no redirects or anything. I’ll consider that again in… idk, a year or something. I would also like to delete my GitHub account (like I have deleted my Twitter account when… *gestures vaguely*), but not only do I need my repos to be up, I also need my account to contribute to projects that are still on GitHub.

One downside of this migration is that since I’m moving off of The Main Forge, my projects are likely to get fewer contributions… But I wasn’t getting many in the first place, and some people have already made accounts on Codeberg to keep contributing to my stuff. Likewise, I’m not really worried about discoverability. We’ll see I guess lol 🤷‍♂️

Lastly, I’m writing this after the migration, and I haven’t really taken notes during it; so, if I’ve forgotten any steps, feel free to let me know in the comments below or by opening an issue, and I’ll edit this article.

Cheers!

Special thanks



Go back to the top of the page