Skip to main content
  1. Posts/

LiveLab: Use Forgejo CI/CD for middleware containers

·3832 words·18 mins·
blog netdevops livelab forgejo python ci_cd git docker flask actions workflow
Maximilian Thoma
Author
Maximilian Thoma
network engineer
Table of Contents

Forgejo is an interesting alternative to GitLab, Gitea, Bitbucket, etc., especially when hosting your build pipeline on-premises in your data center. In this article, I will show you how to use Forgejo with runners to build a CI/CD pipeline, incorporating development branches, test systems, merges, and releases to production systems. This boilerplate setup can be adapted for various scenarios in NetDevOps, providing a flexible and robust solution for managing your development and deployment processes.

At a glance
#

graph LR; X[Developer]-->A subgraph Forgejo A[(GIT)]<-->B[Actions] R[(Releases)] B-->C[Runner] C-->R end subgraph Targetsystems B-->T[Testsystem] B-->P[Prodsystem] R-->T R-->P end

The process begins with a developer who commits code changes to the repository hosted on Forgejo. The GIT repository in Forgejo acts as the central code repository where these changes are pushed. When changes are committed, Forgejo actions are triggered upon various events such as commits, merges, and tag creations. These actions initiate the runner, which is responsible for executing the build and test processes defined in the actions. The runner ensures that the code is compiled, tested, and packaged if necessary.

Once the code passes all tests, it is tagged as a release and is ready to be deployed to the production system. The test system receives the builds for thorough testing, ensuring that all features and fixes work as expected. After thorough testing, a release tag is created. The tagged release is then deployed to both the test system and the production system, ensuring consistency and reliability in the deployment process.

gitGraph commit commit tag: "v1.0.0" branch development checkout development commit branch feature-x checkout feature-x commit commit checkout development merge feature-x commit checkout main merge development tag: "v1.0.1" commit

In my lab repository, the main branch serves as the stable branch. All changes are made in the development branch. These changes are subjected to testing, built, and deployed to the test system via actions workflow whenever a new commit is made. New features are developed as sub-branches from development and then merged back into development. Once the development environment is ready for release, the development branch is merged into main. When a tag is set in the main branch, the actions workflow ensures that the tagged release is rolled out to the production system.

This strategy ensures a streamlined development process, maintaining stability in the main branch while facilitating continuous integration and deployment through the development branch. By following this approach, we can effectively manage and deploy new features and updates with minimal disruption.

Forgejo … what is that???
#

Forgejo Logo

Forgejo is a fork of Gitea, an open-source alternative to GitHub and GitLab for self-hosting. It offers easy deployment with a single binary available for nearly every platform. Forgejo was created in October 2022 after a for-profit company acquired Gitea. It operates under Codeberg e.V., a non-profit organization, and is developed in the public interest. This governance difference has led to significant and lasting changes, making Forgejo distinct from Gitea.

Initially described as a “soft fork” of Gitea, similar to LineageOS from Android, Forgejo is better seen as a product built on Gitea, Git, and many other Free Software projects. The project emphasizes community-driven development, transparency, and open governance, ensuring it remains free from commercial influence and continues to meet the needs of developers worldwide.

Demo environment
#

SystemDescription
forgejoGIT system
runnerACT runner
testsystemTest system
prodsystemProductive system

All machines are running Ubuntu and Docker, except for the GIT system.

Setup Forgejo
#

Get binary
#

Download the binary from codeberg.org, make it executable and move it to /usr/local/bin.

wget https://codeberg.org/forgejo/forgejo/releases/download/v7.0.4/forgejo-7.0.4-linux-amd64
chmod +x forgejo-7.0.4-linux-amd64
mv forgejo-7.0.4-linux-amd64 /usr/local/bin/forgejo

Create user
#

Create a user named git on the system.

 adduser --system --shell /bin/bash --gecos 'Git Version Control' \
  --group --disabled-password --home /home/git  git

Create folders
#

Create the necessary directories and set the appropriate permissions.

mkdir /var/lib/forgejo
chown git:git /var/lib/forgejo && chmod 750 /var/lib/forgejo
mkdir /etc/forgejo
chown root:git /etc/forgejo && chmod 770 /etc/forgejo

Database setup
#

Depending on your deployment size, you may need to set up PostgreSQL or MariaDB. For a small setup, you can use the built-in SQLite. If you choose SQLite, simply select it in the web installer and no additional preparation is required.

See database preparation @ Forgejo

Systemd
#

wget -O /etc/systemd/system/forgejo.service https://codeberg.org/forgejo/forgejo/raw/branch/forgejo/contrib/systemd/forgejo.service

If you’re using MySQL, MariaDB, or PostgreSQL instead of SQLite, you’ll need to open the file (/etc/systemd/system/forgejo.service) and remove the comment markers from the relevant Wants= and After= lines. Otherwise, the default configuration should work fine.

systemctl daemon-reload
systemctl enable --now forgejo.service

Finish inital setup
#

Go to http://<ip>:3000 and complete the setup using the web assistant.

Reverse proxy nginx
#

You can install Nginx to serve as a reverse proxy for Forgejo.

apt install nginx

Create new config in /etc/nginx/conf.d/forgejo.conf.

server {
    listen 80;
    listen [::]:80;
    server_name forgejo.m.local; # Change this to the server domain name.

    location / {
        proxy_pass http://127.0.0.1:3000; # Port 3000 is the default Forgejo port

        proxy_set_header Connection $http_connection;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        client_max_body_size 512M;
    }
}

Next, restart Nginx.

systemctl restart nginx

You must modify /etc/forgejo/app.ini and edit the following settings:

...
# if you are using http
ROOT_URL = http://forgejo.m.lab

# if you are using https
ROOT_URL = https://forgejo.m.lab
...

## Append at the end

[actions]
ENABLED = true

Then, restart the Forgejo service:

systemctl restart forgejo

Setup Forgejo Runner
#

On the runner host, Docker is installed along with the Forgejo runner.

Install docker environment
#

I have released a single-file installer for Docker, which installs both Docker and Docker Compose together.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/lanbugs/docker_installer/HEAD/installer_docker.sh)"

Install forgejo-runner
#

Create a dedicated user for the runner.

adduser --system --shell /bin/bash --gecos 'Forgejo Runner' \
  --group --disabled-password --home /var/lib/forgejo_runner  runner

Allow the runner user access to Docker.

usermod -aG docker runner

Download the Forgejo runner and make it executable.

wget https://code.forgejo.org/forgejo/runner/releases/download/v3.5.0/forgejo-runner-3.5.0-linux-amd64
chmod +x forgejo-runner-3.5.0-linux-amd64
mv forgejo-runner-3.5.0-linux-amd64 /usr/local/bin/forgejo-runner

Switch to the new user and initialize the general configuration. To register the new runner, you need to obtain a token from your Forgejo instance.

Go to Site Administration > Actions > Runners if you want to use the runner globally, or go to Settings > Actions > Runners if you want the runner to be available only for a specific project. Click on Create new runner.

su forgejo_runner -
cd /var/lib/forgejo_runner

# generate general config
forgejo-runner generate-config > config.yaml

# register new runner 
forgejo-runner register

INFO Registering runner, arch=amd64, os=linux, version=v3.5.0.
WARN Runner in user-mode.
INFO Enter the Forgejo instance URL (for example, https://next.forgejo.org/):
https://forgejo.lanbugs.cloud

INFO Enter the runner token:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

INFO Enter the runner name (if set empty, use hostname: runner):
runner.lanbugs.cloud

INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm):
ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-latest

INFO Registering runner, name=runner.lanbugs.cloud, instance=https://forgejo.lanbugs.cloud, labels=[ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-latest].
DEBU Successfully pinged the Forgejo instance server
INFO Runner registered successfully.

Create a new systemd start file at /etc/systemd/system/forgejo_runner.service.

[Unit]
Description=Forgejo Actions runner
Documentation=https://code.forgejo.org/forgejo/runner
After=docker.service

[Service]
ExecStart=/usr/local/bin/forgejo-runner daemon --config /var/lib/forgejo_runner/config.yaml
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/var/lib/forgejo_runner
TimeoutSec=0
RestartSec=10
Restart=always
User=runner
Group=runner

[Install]
WantedBy=multi-user.target

Now we restart the systemd daemon and start the forgejo-runner.service.

systemctl daemon-reload
systemctl enable --now forgejo_runner.service

# check status
systemctl status forgejo_runner.service


● forgejo_runner.service - Forgejo Actions runner
     Loaded: loaded (/etc/systemd/system/forgejo_runner.service; enabled; preset: enabled)
     Active: active (running) since Sun 2024-06-23 06:16:36 UTC; 2s ago
   Main PID: 157704 (forgejo-runner)
      Tasks: 10 (limit: 9252)
     Memory: 6.8M (peak: 7.7M)
        CPU: 85ms
     CGroup: /system.slice/forgejo_runner.service
             └─157704 /usr/local/bin/forgejo-runner daemon --config /var/lib/forgejo_runner/config.yaml

Jun 23 06:16:36 runner systemd[1]: Started forgejo_runner.service - Forgejo Actions runner.
Jun 23 06:16:36 runner forgejo-runner[157704]: time="2024-06-23T06:16:36Z" level=info msg="Starting runner daemon"
Jun 23 06:16:36 runner forgejo-runner[157704]: time="2024-06-23T06:16:36Z" level=info msg="runner: runner.lanbugs.cloud, with version: v3.5.0, with labels: [ubuntu-latest], declared succ>
Jun 23 06:16:36 runner forgejo-runner[157704]: time="2024-06-23T06:16:36Z" level=info msg="[poller 0] launched"

You should see now the new runner in mode idle in the list.

Runner created

Setup demo repository
#

Click on New repository, enter the repository name middleware_app, and check the box for Initialize repository (Adds .gitignore, License and README).

In this demo, the new repository is called m.lab/middleware_app.

Demo code
#

I use a minimal Flask application to showcase the CI/CD functionalities in Forgejo.

app/app.py
#

The code imports the Flask class from the flask module and creates an instance of it called app. It defines a route for the root URL ("/") and associates it with the index function. When this route is accessed, the index function returns a JSON response with the key-value pair {“Hello”: “World”}.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return {"Hello": "World"}

test_app.py
#

The code imports the pytest module and the Flask application instance app from the app.app module. It defines a pytest fixture named client that configures the Flask application for testing and creates a test client. The test_index function uses this test client to send a GET request to the root URL ("/"). It then asserts that the response status code is 200 and that the response JSON data matches {“Hello”: “World”}.

import pytest
from app.app import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client

def test_index(client):
    rv = client.get('/')
    assert rv.status_code == 200
    assert rv.get_json() == {"Hello": "World"}

Dockerfile
#

The Dockerfile begins by specifying the base image python:3.12. It sets Maximilian Thoma as the maintainer. The working directory inside the container is set to /app. The requirements.txt file is copied into the container and its contents are installed using pip3. The start.py file and the app directory are then copied into the container. Port 5000 is exposed for the application. Finally, the container is configured to run gunicorn to serve the Flask application, binding it to all network interfaces on port 5000.

FROM python:3.12

MAINTAINER Maximilian Thoma

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY ./start.py .
COPY ./app ./app

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "start:app"]

start.py
#

The import statement from app.app import app is used to import the Flask application instance from the app/app.py file. This is necessary for Gunicorn to know which application to run.

from app.app import app

requirements.txt
#

These dependencies are installed when the pip3 install -r requirements.txt command is run during the Docker image build process.

flask
gunicorn

pytest.ini
#

The pytest.ini file is configured to have pytest automatically look for test files in the tests directory and add the current directory (.) to the Python path. This ensures that all necessary modules can be imported correctly and the tests run smoothly.

[pytest]
testpaths = tests
pythonpath = .

My demo repository looks like this now:

Demo Repo init

Pipeline: .forgejo/workflows/main.yml
#

To begin, I created a workflow for the main branch to automatically test the code with pytest to ensure it is clean.

The workflow is triggered on any push to the main branch and defines a single job named test that runs on the ubuntu-latest runner.

The steps in the job are as follows:

  1. Check out repository: Uses the actions/checkout@v2 action to check out the repository’s code.
  2. Set up Python: Uses the actions/setup-python@v2 action to set up Python 3.12.
  3. Install dependencies:
    • Upgrades pip.
    • Installs flake8 and pytest using pip.
    • Installs the dependencies listed in requirements.txt.
  4. Run tests: Executes the test suite using pytest.

The workflow ensures that the code is checked out, the Python environment is set up, the necessary dependencies are installed, and the tests are run whenever there is a push to the main branch.

name: Pipeline main branch

on:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - name: Check out repository
      uses: actions/checkout@v2

    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: 3.12

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        pip install -r requirements.txt                                

    - name: Run tests
      run: |
        pytest                      

When you commit the new pipeline to the repository, it will be automatically executed. You can view the action runs in the Actions tab.

Main pipeline

You can unfold Run tests to see the results.

Main pipline tests

Pipeline: .forgejo/workflows/development.yml
#

Now I have built a pipeline that tests the code, builds a Docker image called development-release and deploys this package to the test system. This pipeline is triggered only when commits are made to the development branch.

Trigger:

  • The pipeline runs when there is a push to the development branch.

Jobs:

  1. Test Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. Check out repository: Uses the actions/checkout@v2 action to check out the code from the repository.
      2. Set up Python: Uses the actions/setup-python@v2 action to set up Python version 3.12.
      3. Install dependencies:
        • Upgrades pip.
        • Installs flake8 and pytest.
        • Installs additional dependencies from requirements.txt.
      4. Run tests: Runs tests using pytest.
  2. Build Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. Check out repository: Uses the actions/checkout@v2 action to check out the code from the repository.
      2. Set up QEMU: Uses the docker/setup-qemu-action@v3 action.
      3. Set up Docker Buildx: Uses the docker/setup-buildx-action@v3 action.
      4. Login to Docker registry: Uses the docker/login-action@v3 action with credentials stored in secrets (PRIVATE_REGISTRY_URL, PRIVATE_REGISTRY_USERNAME, and PRIVATE_REGISTRY_PASSWORD).
      5. Build and push Docker image: Uses the docker/build-push-action@v6 action to build and push the Docker image with the tag forgejo.lanbugs.cloud/m.lab/middleware_app:development-release.
  3. Deploy Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. SSH and deploy to production: Uses the appleboy/ssh-action@v1.0.3 action to SSH into the production server (Host: testsystem.lanbugs.cloud) with credentials stored in secrets (SSH_USER and SSH_PASS).
        • Logs into the Docker registry.
        • Changes directory to /opt/dev.
        • Pulls the latest Docker images using docker-compose pull.
        • Brings up the services using docker-compose up -d.

As you can see, some variables and secrets are set in the workflow. These variables and secrets are stored in the project settings, not directly in the workflow file.

name: CI/CD Pipeline Development branch

on:
  push:
    branches:
      - development

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - name: Check out repository
      uses: actions/checkout@v2

    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: 3.12

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        pip install -r requirements.txt        

    - name: Run tests
      run: |
        pytest        

  build:
    runs-on: ubuntu-latest
    steps:

    - name: Check out repository
      uses: actions/checkout@v2

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

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

    - name: Login docker registry
      uses: docker/login-action@v3
      with:
        registry: ${{ secrets.PRIVATE_REGISTRY_URL }}
        username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }}
        password: ${{ secrets.PRIVATE_REGISTRY_PASSWORD }}


    - name: Build and push
      uses: docker/build-push-action@v6
      with:
        push: true
        tags: forgejo.lanbugs.cloud/m.lab/middleware_app:development-release

  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH and deploy to production
        uses: https://github.com/appleboy/ssh-action@v1.0.3
        with:
          host: testsystem.lanbugs.cloud
          username: ${{ secrets.SSH_USER }}
          password: ${{ secrets.SSH_PASS }}
          script: |
            echo "${{ secrets.PRIVATE_REGISTRY_PASSWORD }}" | docker login ${{ secrets.PRIVATE_REGISTRY_URL }} -u "${{ secrets.PRIVATE_REGISTRY_USERNAME }}" --password-stdin
            cd /opt/dev
            docker-compose pull
            docker-compose up -d            

To store variables and credentials, go to Settings > Actions > Secrets in your project to create the required variables. This ensures that sensitive information is securely managed and not exposed in the workflow configuration.

Demo Repo Secrets

Now when you commit to the development branch the workflow will trigger.

Demo repo development deploy

In m.lab > Packages, the development-release Docker container is now available.

development-release

To bind the packages repository to the Git repository, go to settings and select middleware_app.

Link packages

On the test system, there is a docker-compose.yaml file executed by the workflow. It fetches the new Docker image from the package repository and starts it.

version: '3'

services:
  flask-app:
    image: forgejo.lanbugs.cloud/m.lab/middleware_app:development-release
    ports:
      - "80:5000"
    environment:
      - ENV_VAR=development

If you look now on the test system and execute docker ps you see the test container.

# docker ps
CONTAINER ID   IMAGE                                                            COMMAND                  CREATED          STATUS          PORTS                                   NAMES
700da7aecd5d   forgejo.lanbugs.cloud/m.lab/middleware_app:development-release   "gunicorn --bind 0.0…"   18 seconds ago   Up 17 seconds   0.0.0.0:80->5000/tcp, :::80->5000/tcp   dev-flask-app-1

Pipeline: .forgejo/workflows/production.yml
#

The production workflow should only be triggered when a new release is tagged on the main branch. In this case, it should also automatically be tested, build, and pushed to the production system.

Trigger:

  • This workflow is triggered by a push to any tag that matches the pattern v*.*.*.

Jobs:

  1. Test Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. Check out repository: Uses the actions/checkout@v2 action to check out the code from the repository.
      2. Set up Python: Uses the actions/setup-python@v2 action to set up Python version 3.12.
      3. Install dependencies:
        • Upgrades pip.
        • Installs flake8 and pytest.
        • Installs additional dependencies from requirements.txt.
      4. Run tests: Runs tests using pytest.
  2. Build Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. Check out repository: Uses the actions/checkout@v2 action to check out the code from the repository.
      2. Set up QEMU: Uses the docker/setup-qemu-action@v3 action.
      3. Set up Docker Buildx: Uses the docker/setup-buildx-action@v3 action.
      4. Login to Docker registry: Uses the docker/login-action@v3 action with credentials stored in secrets (PRIVATE_REGISTRY_URL, PRIVATE_REGISTRY_USERNAME, and PRIVATE_REGISTRY_PASSWORD).
      5. Build and push Docker image: Uses the action docker/build-push-action@v6 to build and push the Docker image. The image is tagged with latest and the tag name from the Git reference.
  3. Deploy Job:

    • Runs on: ubuntu-latest
    • Steps:
      1. SSH and deploy to production: Uses the appleboy/ssh-action@v1.0.3 action to SSH into the production server (Host: prodsystem.lanbugs.cloud) with credentials stored in secrets (SSH_USER and SSH_PASS).
        • Logs into the Docker registry.
        • Changes directory to /opt/prod.
        • Pulls the latest Docker images using docker-compose pull.
        • Brings up the services using docker-compose up -d.
name: CI/CD Pipeline Main branch build production

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - name: Check out repository
      uses: actions/checkout@v2

    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: 3.12

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        pip install -r requirements.txt        

    - name: Run tests
      run: |
        pytest        

  build:
    runs-on: ubuntu-latest
    steps:

    - name: Check out repository
      uses: actions/checkout@v2

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

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

    - name: Login docker registry
      uses: docker/login-action@v3
      with:
        registry: ${{ secrets.PRIVATE_REGISTRY_URL }}
        username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }}
        password: ${{ secrets.PRIVATE_REGISTRY_PASSWORD }}


    - name: Build and push
      uses: docker/build-push-action@v6
      with:
        push: true
        tags: forgejo.lanbugs.cloud/m.lab/middleware_app:latest,forgejo.lanbugs.cloud/m.lab/middleware_app:${{ github.ref_name }}


  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH and deploy to production
        uses: https://github.com/appleboy/ssh-action@v1.0.3
        with:
          host: prodsystem.lanbugs.cloud
          username: ${{ secrets.SSH_USER }}
          password: ${{ secrets.SSH_PASS }}
          script: |
            echo "${{ secrets.PRIVATE_REGISTRY_PASSWORD }}" | docker login ${{ secrets.PRIVATE_REGISTRY_URL }} -u "${{ secrets.PRIVATE_REGISTRY_USERNAME }}" --password-stdin
            cd /opt/prod
            docker-compose pull
            docker-compose up -d            

To test this workflow I create a new release in the main branch.

Demo release 1

The tag v1.0.0 triggers the workflow production to run. Now are also the latest and v1.0.0 package available.

Demo release latest

The docker-compose.yaml is pretty the same only the label is set to latest.

If you look now on the prod system and execute docker ps you see the prod container.

# docker ps
CONTAINER ID   IMAGE                                                            COMMAND                  CREATED         STATUS         PORTS                                   NAMES
50dd033b6d1e   forgejo.lanbugs.cloud/m.lab/middleware_app:latest                "gunicorn --bind 0.0…"   3 minutes ago   Up 3 minutes   0.0.0.0:80->5000/tcp, :::80->5000/tcp   prod-flask-app-1

Now that all CI/CD workflows are defined and tested, we can start having fun.

Do changes in the development branch
#

Lets start working in the development branch.

Clone the repo
#

git clone git@forgejo.lanbugs.cloud:m.lab/middleware_app.git
Klone nach 'middleware_app'...
remote: Enumerating objects: 37, done.
remote: Counting objects: 100% (37/37), done.
remote: Compressing objects: 100% (24/24), done.
remote: Total 37 (delta 9), reused 0 (delta 0), pack-reused 0
Empfange Objekte: 100% (37/37), fertig.
Löse Unterschiede auf: 100% (9/9), fertig.

Switch to development branch
#

git checkout development
Zu Branch 'development' gewechselt
Ihr Branch ist auf demselben Stand wie 'origin/development'.

Now that we have made some modifications, we want to extend Flask with a new route. Following a test-driven development approach, we first define our test and then build the function.

Therefore, we extend tests/test_app.py with a new route test.

...
def test_newroute(client):
    rv = client.get('/newroute')
    assert rv.status_code == 200
    assert rv.get_json() == {"message": "foobar"}

To prove that pytest works, we can now commit this to the development branch and observe the results.

# add modficications
git add -A
# make a commit
git commit -m "added new test for newroute"
[development 6ad7ec0] add test for new route
 1 file changed, 4 insertions(+)
# push
git push

Objekte aufzählen: 7, fertig.
Zähle Objekte: 100% (7/7), fertig.
Delta-Kompression verwendet bis zu 11 Threads.
Komprimiere Objekte: 100% (3/3), fertig.
Schreibe Objekte: 100% (4/4), 388 Bytes | 388.00 KiB/s, fertig.
Gesamt 4 (Delta 2), Wiederverwendet 0 (Delta 0), Paket wiederverwendet 0 (von 0)
remote:
remote: Create a new pull request for 'development':
remote:   https://forgejo.lanbugs.cloud/m.lab/middleware_app/compare/main...development
remote:
To forgejo.lanbugs.cloud:m.lab/middleware_app.git
   49d55bc..6ad7ec0  development -> development
 

If you now look at Actions, you can see that pytest threw an error.

pytest error

Now we add our new function to app/app.py and pytest will be happy :-)

...
@app.route("/newroute")
def newroute():
    return {"message": "foobar"}

pytest ok

The test system is working correctly, and the application has been tested so far. Of course, after deployment to the test system, you can add additional steps to automatically test the functions of your app to ensure all cases are working correctly.

But for now, let’s proceed to the release.

Merge development to main branch
#

To merge your developments into the main branch, you can use the website.

Generate pull request
#

Select the pull direction; we want to pull from development to main.

Pull request

Below, you can now see what will be included in the pull request.

Pull details

Click on New pull request, then you can add a description of your changes and click Create pull request.

Pull created

Below the commits in the pull request, you can see that the CI/CD tests have already passed from the last commit, so you can directly merge your changes to the main branch. If the tests are not completed, it will not allow the merge.

Click on Create merge commit. Now you can summarize your merge commit. Do not tick ‘Delete branch’, because we will always use the development branch to work on new features.

Merge commit

Now you will see on the commit graph the merge.

Merge ready

Release new version
#

Everything is tested, and we have our change window to roll out the new release. Goto Releases and click New Release.

New Release

In the release list, the new one now appears. You can see the orange ball behind the title, indicating that a workflow is running, which tests, builds, and deploys the new container to production.

Release

Tip
#

You can prevent direct commits to the main branch in Forgejo. To configure this, go to your repository’s Settings > Branches and add a new branch protection rule.

Set only the Protected branch name pattern to main and leave all other settings at their default values.

This will prevent direct commits to the main branch, allowing only merges.

Conclusion
#

In this article, I demonstrated how to use Forgejo as a CI/CD tool for managing middleware container deployments. Forgejo, a community-driven alternative to GitLab and Bitbucket, is perfect for on-premises build pipelines.

I walked through setting up Forgejo and its runners, creating workflows for development and production branches, and deploying applications using Docker containers. This setup ensures automated testing and streamlined deployment, enhancing code quality and reducing downtime.

By following this guide, you can implement a similar CI/CD pipeline tailored to your needs, benefiting from a stable main branch and continuous integration through the development branch. Forgejo’s open governance and community-driven development ensure it will continue to meet developers’ needs worldwide.

Try Forgejo for your next project and enjoy a reliable, efficient CI/CD pipeline.

Happy automating! 😊

References
#

Related

LiveLab: Cisco PnP provisioning with Netbox
·2707 words·13 mins
blog netdevops livelab cisco pnp netbox middleware flask
If you’re tired of feeling like a circus performer juggling flaming torches when provisioning Cisco devices, it’s time to meet your new best friend: Netbox.
LiveLab: Automating Excel Reports with Ansible, Flask, Mongodb, TextFSM and Pandas
·2081 words·10 mins
blog netdevops livelab ansible excel flask mongodb textfsm pandas
Today, I want to show you how I use Ansible, Flask, MongoDB, TextFSM and Pandas to generate Excel reports.
LiveLab: Netbox VLAN deployment to Cisco devices
·1886 words·9 mins
netdevops livelab blog python netbox apiflask
Welcome to my latest blog post where I’ll be taking you through an exciting demonstration of integrating NetBox with custom middleware to automate VLAN deployments on Cisco devices.