Published: Dec 5, 2023 by Isaac Johnson
I came across Forgejo while searching for a different tool recently. The word is a bit unusual. According to their site it comes from the Esperanto word for “Forge” ( forĝejo ), their mantra being “We Forge”.
Forgejo visually looks like Gitea because it is based on a fork of Gitea. The fork was created when a for-profit “Gitea Ltd” company formed in 2022 and the OS community became worried Gitea would it would move off OS. However, in the year+ since it happened, Gitea really has remained Open Source to a large extent and kept the MIT license (quite a discussion when they went for-profit as seen here).
Today I wanted to setup Forgejo and see how well it works. We will first just use a Docker image, then move to an HA setup with Kubernetes, TLS ingress and MariaDB (MySQL) hosted on a NAS for a production instance. During the course, We’ll compare it with Gitea in parts (and actually marry the two together for some replication). We’ll also explore Gitea/Forgejo workflows. Let’s dig in!
Forgejo
To run with docker, we first need to create a directory then run
$ mkdir forgejo
$ docker run --name forgejotest -p 3300:3000 -p 222:22 -v $(pwd)/forgejo:/data -v /etc/timezone:/etc/timezone:ro -v /etc/localtime:/etc/localtime:ro -e USER_UID=1000 -e USER_GID=1000 codeberg.org/forgejo/forgejo:1.20
Unable to find image 'codeberg.org/forgejo/forgejo:1.20' locally
1.20: Pulling from forgejo/forgejo
96526aa774ef: Pull complete
70d6a3ceec46: Pull complete
3b12062cc7ad: Pull complete
a8f7e561dc2d: Pull complete
334bba0d8ebe: Pull complete
96bd3dd100d1: Pull complete
0891a06db1aa: Pull complete
63970971aebc: Pull complete
a512d230781b: Pull complete
4f4fb700ef54: Pull complete
70b92d8af2b8: Pull complete
Digest: sha256:d665129e66cc04fa72aa6a284eb98b17c0eee642aeaad7c88edec21f9edbf519
Status: Downloaded newer image for codeberg.org/forgejo/forgejo:1.20
Generating /data/ssh/ssh_host_ed25519_key...
Generating /data/ssh/ssh_host_rsa_key...
Generating /data/ssh/ssh_host_ecdsa_key...
Server listening on :: port 22.
Server listening on 0.0.0.0 port 22.
2023/11/20 18:52:27 cmd/web.go:223:runWeb() [I] Starting Gitea on PID: 15
2023/11/20 18:52:27 cmd/web.go:106:serveInstall() [I] Gitea version: 1.20.5+0 built with GNU Make 4.4.1, go1.20.8 : bindata, timetzdata, sqlite, sqlite_unlock_notify
2023/11/20 18:52:27 cmd/web.go:107:serveInstall() [I] App path: /usr/local/bin/gitea
2023/11/20 18:52:27 cmd/web.go:108:serveInstall() [I] Work path: /data/gitea
2023/11/20 18:52:27 cmd/web.go:109:serveInstall() [I] Custom path: /data/gitea
2023/11/20 18:52:27 cmd/web.go:110:serveInstall() [I] Config file: /data/gitea/conf/app.ini
2023/11/20 18:52:27 cmd/web.go:111:serveInstall() [I] Prepare to run install page
2023/11/20 18:52:27 cmd/web.go:285:listen() [I] Listen: http://0.0.0.0:3000
2023/11/20 18:52:27 cmd/web.go:289:listen() [I] AppURL(ROOT_URL): http://localhost:3000/
2023/11/20 18:52:27 ...s/graceful/server.go:62:NewServer() [I] Starting new Web server: tcp:0.0.0.0:3000 on PID: 15
This will launch us into a configuration page
I left most things default and started the install
which started the loading
Because I didn’t set the admin password in settings, the first created user became admin
There are a lot of nice admin features such as deactivating accounts and archiving repo
Right now we have no repos shared publicly
I’ll create a test public repo (TestPublic) by clicking “New Repository”
and giving it a name
I can see it’s public by checking in an Incognito window
I can now create an empty dir, add a file, and push
builder@LuiGi17:~/Workspaces$ mkdir TestPublic
builder@LuiGi17:~/Workspaces$ cd TestPublic/
builder@LuiGi17:~/Workspaces/TestPublic$ echo "# Test" > README.md
builder@LuiGi17:~/Workspaces/TestPublic$ git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
Initialized empty Git repository in /home/builder/Workspaces/TestPublic/.git/
builder@LuiGi17:~/Workspaces/TestPublic$ git checkout -b main
Switched to a new branch 'main'
builder@LuiGi17:~/Workspaces/TestPublic$ git add README.md
builder@LuiGi17:~/Workspaces/TestPublic$ git commit -m 'first commit'
[main (root-commit) 6f8e38f] first commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
builder@LuiGi17:~/Workspaces/TestPublic$ git remote add origin http://192.168.1.100:3300/isaac/TestPublic.git
builder@LuiGi17:~/Workspaces/TestPublic$ git push -u origin
fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin main
builder@LuiGi17:~/Workspaces/TestPublic$ git push -u origin main
Username for 'http://192.168.1.100:3300': isaac@freshbrewed.science
Password for 'http://isaac@freshbrewed.science@192.168.1.100:3300':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 220 bytes | 110.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://192.168.1.100:3300/isaac/TestPublic.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
And I can see the file there
Because we can reach it with the raw
endpoint, we could easily pull files down with wget or curl as well
builder@LuiGi17:~/Workspaces$ wget http://192.168.1.100:3300/isaac/TestPublic/raw/branch/main/README.md
--2023-11-20 20:14:36-- http://192.168.1.100:3300/isaac/TestPublic/raw/branch/main/README.md
Connecting to 192.168.1.100:3300... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7 [text/plain]
Saving to: ‘README.md’
README.md 100%[=======================================================================>] 7 --.-KB/s in 0s
2023-11-20 20:14:36 (187 KB/s) - ‘README.md’ saved [7/7]
builder@LuiGi17:~/Workspaces$ cat README.md
# Test
We could clone with SSH keys as well. First we need to add our private key by clicking “Add Key”
I’ll add the contents of my .pub
file in ~.ssh
Which I can see now listed
It’s a bit different URL when cloning with a custom port, but it works
builder@LuiGi17:~/Workspaces$ git clone ssh://git@192.168.1.100:222/isaac/TestPublic.git Test2
Cloning into 'Test2'...
The authenticity of host '[192.168.1.100]:222 ([192.168.1.100]:222)' can't be established.
ED25519 key fingerprint is SHA256:LECyhFpc385yhunb07LWxCDtVrcxyvfxbks1C/x6Xf0.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.1.100]:222' (ED25519) to the list of known hosts.
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
Let’s create a wiki
I can then write some markdown content
I can save and see the rendered content
Then create a project
By default, we see just one Column in our Kanban page
I’ll create an Active column
I then created a few more
Then create a test issue
and see details
I can then move between states
Mysql/MariaDB Database
Before I launch an HA instance, I really want a proper backend database. I typically use PostgreSQL (which runs on a VM) or MariaDB which can run on either one of my NASes.
I’ll create a user on the NAS by SSH’ing (and after loading the MariaDB 10 package)
ijohnson@SassyNassy:~$ /usr/local/mariadb10/bin/mysql -u root -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 15
Server version: 10.3.32-MariaDB Source distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> CREATE USER 'forgejo' IDENTIFIED BY 'n0tmypassw0rd';
older MySQL instances would have us use:
CREATE USER 'forgejo':'%' IDENTIFIED BY 'n0tmypassw0rd';
But I did not need that. In fact, I can be sure it worked by checking the Grants
MariaDB [(none)]> SHOW GRANTS FOR forgejo@'%';
+--------------------------------------------------------------------------------------------------------+
| Grants for forgejo@% |
+--------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `forgejo`@`%` IDENTIFIED BY PASSWORD '*9DF4E2EB962EC499A5907589C7E8B92E320397C8' |
+--------------------------------------------------------------------------------------------------------+
1 row in set (0.201 sec)
Now I’ll set some Grants and flush the privs
MariaDB [(none)]> CREATE DATABASE forgejodb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
Query OK, 1 row affected (0.205 sec)
MariaDB [(none)]> GRANT ALL PRIVILEGES ON forgejodb.* TO 'forgejo';
Query OK, 0 rows affected (0.048 sec)
MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.060 sec)
I also went back and explicitly granted to all hosts in trying to debug connectivity
MariaDB [(none)]> GRANT ALL PRIVILEGES ON forgejodb.* TO 'forgejo'@'%' IDENTIFIED BY 'n0tmypassw0rd';
Query OK, 0 rows affected (0.320 sec)
I did find I needed to pass the MsSQL port which matched the settings on the NAS:
builder@DESKTOP-QADGF36:~$ mysql -u forgejo -h sassynassy -p forgejodb -P 3307
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 18
Server version: 10.3.32-MariaDB Source distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [forgejodb]>
Helm Install
The helm chart is based on Gitea. We can use the same structure here
$ cat forgego.values
gitea:
config:
APP_NAME: 'Forgejo: With a cup of tea.'
gitea:
config:
database:
DB_TYPE: mysql
HOST: 192.168.1.129:3307
NAME: forgejodb
USER: forgejo
PASSWD: 'asdfasdfasdfasdf'
SCHEMA: gitea
postgresql:
enabled: false
service:
ssh:
type: ClusterIP
port: 222
I didn’t want to use default 22 for SSH as that makes a mess of things. Also, my MariaDB doesn’t run on the standard 3306, rather port 3307 so I assumed I could add port to the hostname.
I then created a namespace and launched the chart
$ kubectl create ns forgejo
namespace/forgejo created
$ helm install -n forgejo forgejo -f forgego.values oci://codeberg.org/forgejo-contrib/forgejo
Pulled: codeberg.org/forgejo-contrib/forgejo:0.14.0
Digest: sha256:6f510f4b1cb079eb6b321c2d868255d60fdb9a365e648b517c61b42a3334271a
coalesce.go:175: warning: skipped value for memcached.initContainers: Not a table.
NAME: forgejo
LAST DEPLOYED: Tue Nov 21 13:40:45 2023
NAMESPACE: forgejo
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
echo "Visit http://127.0.0.1:3000 to use your application"
kubectl --namespace forgejo port-forward svc/forgejo-http 3000:3000
As expected, I see pods and services now launched
$ kubectl get pods -n forgejo
NAME READY STATUS RESTARTS AGE
forgejo-memcached-6fd98dddbb-prx7g 1/1 Running 0 3m56s
forgejo-0 1/1 Running 0 3m56s
$ kubectl get svc -n forgejo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
forgejo-ssh ClusterIP None <none> 222/TCP 4m9s
forgejo-http ClusterIP None <none> 3000/TCP 4m9s
forgejo-memcached ClusterIP 10.43.108.161 <none> 11211/TCP 4m9s
I’ll then create an A Record to point in for forgejo
$ cat r53-forgejo.json
{
"Comment": "CREATE forgejo fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "forgejo.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-forgejo.json
{
"ChangeInfo": {
"Id": "/change/C00925091P54X4SDFXR7U",
"Status": "PENDING",
"SubmittedAt": "2023-11-21T19:47:34.835Z",
"Comment": "CREATE forgejo fb.s A record "
}
}
The Forgejo ingress will solve HTTP/S but not SSH. Essentially, SSH will be limited to in cluster operations for now and those can go to the service (forgejo-ssh.forgejo.svc.cluster.local:222)
$ cat forgejo.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingress.kubernetes.io/proxy-body-size: "0"
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.org/client-max-body-size: "0"
nginx.org/proxy-connect-timeout: "600"
nginx.org/proxy-read-timeout: "600"
generation: 1
labels:
app: forgejo
app.kubernetes.io/instance: forgejo
app.kubernetes.io/name: forgejo
name: forgejo
spec:
rules:
- host: forgejo.freshbrewed.science
http:
paths:
- backend:
service:
name: forgejo-http
port:
number: 3000
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- forgejo.freshbrewed.science
secretName: forgejo-tls
$ kubectl apply -f forgejo.ingress.yaml -n forgejo
ingress.networking.k8s.io/forgejo created
I can now login and register a new user (which will be the admin)
The downside was this user was not an admin!
I added the missing block for the super user account
$ cat ~/forgego.values
gitea:
config:
APP_NAME: 'Forgejo: With a cup of tea.'
gitea:
config:
database:
DB_TYPE: mysql
HOST: 192.168.1.129:3307
NAME: forgejodb
USER: forgejo
PASSWD: 'asdfasdfasdf'
SCHEMA: gitea
admin:
username: "builderadmin"
password: "asdfasdfasfsadf"
email: "isaac.johnson..@gmail.com"
postgresql:
enabled: false
service:
ssh:
type: ClusterIP
port: 222
I’m hoping I can upgrade
$ helm upgrade -n forgejo forgejo -f /home/builder/forgego.values oci://codeberg.org/forgejo-contrib/forgejo
Pulled: codeberg.org/forgejo-contrib/forgejo:0.14.0
Digest: sha256:6f510f4b1cb079eb6b321c2d868255d60fdb9a365e648b517c61b42a3334271a
coalesce.go:175: warning: skipped value for memcached.initContainers: Not a table.
Release "forgejo" has been upgraded. Happy Helming!
NAME: forgejo
LAST DEPLOYED: Tue Nov 21 14:00:50 2023
NAMESPACE: forgejo
STATUS: deployed
REVISION: 2
NOTES:
1. Get the application URL by running these commands:
echo "Visit http://127.0.0.1:3000 to use your application"
kubectl --namespace forgejo port-forward svc/forgejo-http 3000:3000
That worked!
I realized that since this is based on Gitea, I could then add mailer and server blocks to solve the URL complaints:
The new values
$ cat ~/forgego.values
gitea:
config:
APP_NAME: 'Forgejo: With a cup of tea.'
gitea:
config:
database:
DB_TYPE: mysql
HOST: 192.168.1.129:3307
NAME: forgejodb
USER: forgejo
PASSWD: 'adsfasdfasdfasdfasdf'
SCHEMA: gitea
actions:
ENABLED: true
mailer:
ENABLED: true
FROM: isaac@freshbrewed.science
MAILER_TYPE: smtp
PASSWD: SG.asdfsdfasdfsadfasdfasdfasdfasdfasdfasdfasdfasdfasdf
SMTP_ADDR: smtp.sendgrid.net
SMTP_PORT: 465
USER: apikey
openid:
ENABLE_OPENID_SIGNIN: false
ENABLE_OPENID_SIGNUP: false
server:
DOMAIN: forgejo.freshbrewed.science
ROOT_URL: https://forgejo.freshbrewed.science/
service:
DISABLE_REGISTRATION: true
admin:
username: "builderadmin"
password: "adfasdfasdfasdfasdfasdf"
email: "isaac.johnson@gmail.com"
postgresql:
enabled: false
service:
ssh:
type: ClusterIP
port: 222
It’s a little upset about the MAILER_TYPE setting, but other than that it’s good.
I now have sign-in, but not registration
Note: it’s sad really - i would love to just keep it open, but then spammers just abuse the snot out of it
I’ll make a new public repo
And now I can also enable actions (which are based on Github actions)
Runners
Get the token
On a host in our network we create a config.yaml
builder@builder-T100:~/Forgejo$ cat config.yaml
# Example configuration file, it's safe to copy this as the default config file without any modification.
# You don't have to copy this file to your instance,
# just run `./act_runner generate-config > config.yaml` to generate a config file.
log:
# The level of logging, can be trace, debug, info, warn, error, fatal
level: info
runner:
# Where to store the registration result.
file: .runner
# Execute how many tasks concurrently at the same time.
capacity: 1
# Extra environment variables to run jobs.
envs:
A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2
# Extra environment variables to run jobs from a file.
# It will be ignored if it's empty or the file doesn't exist.
env_file: .env
# The timeout for a job to be finished.
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
timeout: 3h
# Whether skip verifying the TLS certificate of the Gitea instance.
insecure: false
# The timeout for fetching the job from the Gitea instance.
fetch_timeout: 5s
# The interval for fetching the job from the Gitea instance.
fetch_interval: 2s
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
# Like: ["macos-arm64:host", "ubuntu-latest:docker://node:16-bullseye", "ubuntu-22.04:docker://node:16-bullseye"]
# If it's empty when registering, it will ask for inputting labels.
# If it's empty when execute `deamon`, will use labels in `.runner` file.
labels:
- ubuntu-22.04:host
cache:
# Enable cache server to use actions/cache.
enabled: true
# The directory to store the cache data.
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
dir: ""
# The host of the cache server.
# It's not for the address to listen, but the address to connect from job containers.
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
host: ""
# The port of the cache server.
# 0 means to use a random available port.
port: 0
# The external cache server URL. Valid only when enable is true.
# If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
# The URL should generally end with "/".
external_server: ""
container:
# Specifies the network to which the container will connect.
# Could be host, bridge or the name of a custom network.
# If it's empty, act_runner will create a network automatically.
network: ""
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
privileged: true
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
options:
# The parent directory of a job's working directory.
# If it's empty, /workspace will be used.
workdir_parent:
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
# valid_volumes:
# - data
# - /src/*.json
# If you want to allow any volume, please use the following configuration:
# valid_volumes:
# - '**'
valid_volumes: []
# overrides the docker client host with the specified one.
# If it's empty, act_runner will find an available docker host automatically.
# If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
docker_host: ""
host:
# The parent directory of a job's working directory.
# If it's empty, $HOME/.cache/act/ will be used.
workdir_parent:
We can then use the token to then launch a runner
builder@builder-T100:~/Forgejo$ docker run -v /home/builder/Forgejo/config.yaml:/config.yaml -v /home/builder/Forgejo/data:/data -v /var/run/docker.sock:/var/run/docker.sock -e CONFIG_FILE=/config.yaml -e GITEA_INSTANCE_URL=https://forgejo.freshbrewed.science/ -e GITEA_RUNNER_REGISTRATION_TOKEN=AjF1ERsEzZHMgGbXnuGUmki8fbVsXrZwsswZqvZs -e GITEA_RUNNER_NAME=buildert100 -e GITEA_RUNNER_LABELS=onprem --name buildert100gf -d gitea/act_runner:nightly
5abdc928e11dd96a83662ea2735b83673df1d66532829f94def97b79b477e8ce
Then we can see it launched
Testing the runner
We can create a workflow in much the same manner as Gitea
builder@LuiGi17:~/Workspaces/MyPublicRepo/.gitea/workflows$ ls
test2.yaml test3.yaml
builder@LuiGi17:~/Workspaces/MyPublicRepo/.gitea/workflows$ cat test2.yaml
name: TestRunner2
run-name: $ building test
on: [push]
jobs:
TestRunner:
runs-on: ubuntu-22.04
steps:
- name: apt
run: |
hostname
# if running as non-root, add sudo
echo test
builder@LuiGi17:~/Workspaces/MyPublicRepo/.gitea/workflows$ cat test3.yaml
name: TestRunner3
run-name: $ building test
on: [push]
jobs:
TestRunner:
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Setup Docker
run: |
# if running as non-root, add sudo
apt-get update && \
apt-get install -y \
ca-certificates \
curl \
gnupg \
sudo \
lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt install -y docker-ce-cli
docker run hello-world
#apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- name: Test Docker
run: |
# if running as non-root, add sudo
echo hi
docker ps
Here we have two workflows. Test2
does effectively nothing and Test3
will setup docker on a current container and run a “Hello World” test image
We can see they run:
Test2 just shows the hostname
and Test3 will show it installs the Docker CLI and runs the Hello World image. This is possible because we mounted the docker sockets when we launched it on the host.
Mirroring
First, let’s setup a repo in Gitea that will be our sync target
I now have an uninitialized target repo we can use
I’ll now add that as the sync target in Forgejo
and once saved, see it confirmed
Clicking Sync Now tells me it’s in progress
Then synced
We can sync the other way as well.
Here, I’ll create a repo in Forgejo
And then use it as a Mirror destination in Gitea
With a push, I now have a full backup of the contents
Webhooks to Matrix
We can add a Matrix webhook for events
now events push notifications
Gitea vs Forgejo
They seem so similar, what really is the difference?
This stems from October 2022 when the Open-Source suite was transferred to a for-profit company. Forgejo, the Esperanto word for forge, was created.
Gitea has a commercial side
whereas Forgejo does not and it’s trademarks are held in trust by a German not-for-profit, Codeberg e.V. (eingetragener Verein)
Because Gitea has held MIT licensing, Forgejo is free to pull down the source (and in their opinion) improve upon it and secure it.
Summary
I really like Forgejo. I’m really stuck in picking between Forgejo and Gitea. Because Gitea dropped MySQL (MariaDB) support and my NAS doesnt natively carry PosgreSQL, I might well lean into Forgejo. I do think there is value in having dual GIT systems that sync to each other. I might even explore at a later date some kind of regular restorable offsite backups.
Even though both Gitea workflows and Forgejo workflows, in my demo, used the SAME Gitea container image, I routinely had better performance from Forgejo. It just worked. Gitea workflows, at least for me, often get stuck and i have to cancel and “re-run all jobs” to get them going again. However, it could just be a nuance of my particular version of Gitea.