Skip to content

Branch Structure & Git Workflow

This project follows a strict Gitflow-inspired workflow to ensure code quality and stability. Our CI/CD pipeline enforces formatting, linting, and commit styles.


Branching Strategy

We use a hierarchical branching model. Please adhere to the following naming conventions strictly.

gitGraph
   commit id: "v0.9.0" tag: "v0.9.0"

   branch develop
   checkout develop
   commit id: "init develop"

   branch dev/tables
   checkout dev/tables
   commit id: "feat(api): tables client"
   commit id: "feat(api): add caching"
   checkout develop
   merge dev/tables

   branch dev/backend/database
   checkout dev/backend/database
   commit id: "feat(db): migrations"
   checkout develop
   merge dev/backend/database

   branch fix/pipeline_fail
   checkout fix/pipeline_fail
   commit id: "fix(ci): lint config"
   checkout develop
   merge fix/pipeline_fail

   branch dev/mkdocs
   checkout dev/mkdocs
   commit id: "docs: setup guide"
   checkout develop
   merge dev/mkdocs

   checkout main
   merge develop tag: "v1.0.0"

   branch hotfix/v1.0.1
   checkout hotfix/v1.0.1
   commit id: "fix(critical): memory leak"
   checkout main
   merge hotfix/v1.0.1 tag: "v1.0.1"
   checkout develop
   merge hotfix/v1.0.1

Branch Types

We distinguish between protected integration branches and temporary working branches.

Prefix Usage Example
main Production Code. Only stable releases. Deploys to registry with Production Secrets. main
develop Integration Branch. All features merge here first. develop
dev/* Features. Implementation of new functionality or sub-projects. dev/tables
dev/backend/database
fix/* Bugfixes. Corrections for existing bugs. fix/pipeline_fail
fix/login_error
hotfix/* Critical Fixes. Urgent patches directly on main, generating a new patch release. hotfix/v1.0.1

Warning

Do not push directly to main or develop. Always use a Merge Request.


Git Workflow

1. Setup & Sync

Always start your work from the latest develop state to avoid conflicts.

See the Quick Setup Guide in the developer documentation for installation instructions.

# Quick refresher:
git checkout develop
git pull origin develop
git checkout -b dev/my-feature-name

2. Commit Guidelines (Strictly Enforced)

We follow Conventional Commits. The CI pipeline (commitlint) will reject your push if the format is incorrect.

Format: <type>(<scope>): <description>

Type Description Example
feat New feature feat(ui): add new timetable view
fix Bug fix fix(db): resolve connection timeout
docs Documentation only docs(readme): update setup steps
style Formatting style(lint): fix pylint errors
refactor Code change that fixes no bug/adds no feature refactor(auth): simplify login logic
test Adding missing tests test(api): add unit test for login
chore Build process, aux tools chore(ci): update pipeline image

3. Quality Checks (Local)

Before pushing, ensure your code passes our strict quality gates locally to save CI time.

  • pylint Must achieve a score of ≥ 9.5.

    pylint --recursive=y src
    
  • Type Checking: We use basedpyright (configured in pyproject.toml).

Check pyproject.toml
pyproject.toml
[build-system]
requires = ["hatchling >= 1.27"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/oscar", "src/util"]

# https://docs.basedpyright.com/latest/configuration/config-files/
[tool.basedpyright]
"typeCheckingMode" = "recommended"
# "reportUnusedFunction" = false

[tool.pylint]
# disable = "unused-function"
[tool.pylint.options]
good-names-rgx = "(_[a-z_]*|command|group|listen)"

[tool.pylint.messages_control]
disable = [
    "C0114", # missing-module-docstring
    "C0115", # missing-class-docstring
    "C0116", # missing-function-docstring
    "R0903", # too-few-public-methods
    "W0511", # fixme (TODOs)
    "R0801", # duplicate-code
]


[project.scripts]
oscar_ovgu = "oscar.main:main"

[project]
name = "oscar_ovgu"
version = "1.0.0"
authors = [
  { name="ISG", email="jana.goers@ovgu.de" },
]
description = "Organized Study Choice & Academic Roadmapper"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "discord-py",
    "python-dotenv",
    "loguru",
    "cyclopts",
    "requests",
    "rapidfuzz",
    "translate",
    "matplotlib>=3.9.4",
    "numpy",
    "toml"
]
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
]
license = "Apache-2.0"
license-files = ["./license"]

[project.urls]
Homepage = "https://www.fin.ovgu.de/"
Issues = "https://www.fin.ovgu.de/Fakultaet.html"

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-mock>=3.12.0",
    "pytest-cov>=4.0.0",
    "pylint>=3.0.0",
    "pytest-asyncio>=0.23.0",
]
docs = [
    "mkdocs-material>=9.6.20",
    "mkdocstrings[python]>=0.30.1",
    "mkdocs-glightbox",
    "mkdocs-macros-plugin",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
asyncio_mode = "auto"
filterwarnings = [
    "ignore::DeprecationWarning:discord",
]

4. Push & Merge

Push your branch and open a Merge Request targeting develop.


CI/CD Pipeline & Deployment

The .gitlab-ci.yml pipeline handles verification and deployment automatically.

Check .gitlab-ci.yml
.gitlab-ci.yml
# based on: https://docs.gitlab.com/user/packages/package_registry/pypi_cosign_tutorial/#set-up-a-python-project
variables:
  # Base Python version for all jobs
  PYTHON_VERSION: "3.14-trixie"
  # Package names and versions
  PACKAGE_NAME: "oscar_ovgu"
  PYLINT_FAIL_THRESHOLD: 9.5
  # Pip and uv cache directory for faster builds
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
  UV_CACHE_DIR: "$CI_PROJECT_DIR/.uv-cache"


# Template for Python-based jobs
.python-job:
  image:
    name: python:${PYTHON_VERSION}
    pull_policy: if-not-present
  before_script:
    # Then install Python dependencies
    - pip install uv
    - uv pip install --system build twine setuptools wheel
    - uv pip install --system .[dev,docs]
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - ${PIP_CACHE_DIR}
      - ${UV_CACHE_DIR}

stages:
  - lint
  - test
  - build
  - publish


pylint:
  extends: .python-job
  stage: lint
  script:
    # - uv pip install --system .[dev]
    # as of october 2025, the --jobs flag produces different scores for different values
    - pylint --recursive=y --fail-under $PYLINT_FAIL_THRESHOLD src
    - echo "Exit Code is $?"
  after_script:
    - >
      if [ $CI_JOB_STATUS == "failed" ]; then
        echo "Pylint Score too low for build, please improve style and commit again."
      fi

commitlint:
  stage: lint
  image: registry.hub.docker.com/library/node:alpine
  before_script:
    - apk add --no-cache git
    - npm install --save-dev @commitlint/config-conventional @commitlint/cli
  script:
    - |
      if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
        npx commitlint --from HEAD~1 --to HEAD --verbose
      else
        npx commitlint --from $CI_COMMIT_BEFORE_SHA --to HEAD --verbose
      fi
  after_script:
    - >
      if [ "$CI_JOB_STATUS" = "failed" ]; then
        echo "Commit messages must follow Conventional Commits, see docs/developer/workflow.md.";
        echo "Keep the header at 120 characters or less. Long body lines only warn.";
      fi

pytest:
  extends: .python-job
  stage: test
  script:
    - pytest --cov=src --cov-report=term-missing --cov-report=html:coverage_html
  coverage: '/TOTAL.*\s+(\d+%)/'
  artifacts:
    paths:
      - coverage_html/
    expire_in: 30 days

docs-test:
  extends: .python-job
  stage: test
  rules:
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
  script:
    - mkdocs build --strict

build:
  extends: .python-job
  stage: build
  script:
    # Initialize git repo with actual content
    # - echo $CI_COMMIT_DESCRIPTION


    # Debug: show updated file
    - echo "Updated pyproject.toml contents:"
    - cat pyproject.toml

    # Build package
    - python -m build
  artifacts:
    paths:
      - dist/
      - pyproject.toml

container_build_and_publish:
  image:
    name: quay.io/buildah/stable:latest
    pull_policy: if-not-present
  stage: publish
  needs:
    - build
  variables:
    STORAGE_DRIVER: vfs
    IMAGE: 192.168.1.15:5050/studium-lehre/discord-bot
  # The server starts the image without its own env, so the secrets are baked in (as before 3ce4726).
  # Without them the bot crashes on login and podman auto-update silently rolls back.
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

  before_script:
    - ls dist
    - echo "ref protected - $CI_COMMIT_REF_PROTECTED"
    - |
      missing=""
      for var in BOT_TOKEN DISCORD_SERVER_ID TABLES_URL TABLES_USERNAME TABLES_PASSWORD; do
        eval "value=\${$var}"
        if [ -z "$value" ]; then
          missing="$missing $var"
        else
          echo "$var is set (${#value} chars)"
        fi
      done
      if [ -n "$missing" ]; then
        echo "not available in this pipeline:$missing - refusing to publish an image that can't log in"
        exit 1
      fi
  script:
    - echo "$CI_REGISTRY_PASSWORD" | buildah login 192.168.1.15:5050 -u $CI_REGISTRY_USER --password-stdin --tls-verify=false
    - >
      buildah build --tls-verify=false
      --build-arg "OSCAR_REVISION=$CI_COMMIT_SHORT_SHA"
      --build-arg "BOT_TOKEN=$BOT_TOKEN"
      --build-arg "DISCORD_SERVER_ID=$DISCORD_SERVER_ID"
      --build-arg "TABLES_URL=$TABLES_URL"
      --build-arg "TABLES_USERNAME=$TABLES_USERNAME"
      --build-arg "TABLES_PASSWORD=$TABLES_PASSWORD"
      -t "$IMAGE:latest" -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
    # the server auto-pulls :latest, the sha tag makes rollbacks possible
    - buildah push --tls-verify=false $IMAGE:latest
    - buildah push --tls-verify=false $IMAGE:$CI_COMMIT_SHORT_SHA

pages:
  extends: .python-job
  stage: publish
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - ls
    # - uv pip install --system .[docs]
    - mkdocs build --site-dir public
  artifacts:
    paths:
      - public

OSCAR Pipeline

Pipeline Stages

  1. Lint (lint):
    • Runs pylint (Fails if score < 9.5).
    • Runs commitlint (Checks commit message format).
  2. Build (build):
    • Builds the Python package (Wheel/Source) and updates pyproject.toml metadata.
  3. Publish (publish):
    • Container Image: Builds the OCI image using buildah and pushes to the internal OVGU registry (isggit3.cs.ovgu.de:5050/studium-lehre/discord-bot).
    • Documentation: Builds MkDocs and deploys to GitLab Pages.

Secret Management & Environments

The build process automatically injects the correct configuration based on the branch:

  • main Branch:
  • Uses Production Tokens ($BOT_TOKEN).
  • Deploys to the production namespace (studium-lehre/discord-bot).
  • Other Branches (e.g., dev/*):
  • Uses Development Tokens ($DEV_BOT_TOKEN).
  • Safe for testing without affecting the live bot.

Note: The Containerfile relies on these secrets being passed via CI/CD environment variables securely during the build and runtime. Do not commit secrets to the repository.


Automated Releases & Versioning

OSCAR uses automated Semantic Versioning based on Conventional Commits since the last release tag:

  • feat bumps the MINOR version (e.g., v0.2.0 -> v0.3.0).
  • fix or perf bumps the PATCH version (e.g., v0.2.0 -> v0.2.1).
  • BREAKING CHANGE or ! bumps the MAJOR version (e.g., v0.2.0 -> v1.0.0).
  • All other commit types (docs, style, chore, etc.) do not trigger a release bump.

Release Tool (tools/release_version.py)

The repository includes a dedicated release tool that inspects Git history:

  • Dry run:
    python tools/release_version.py
    
    Inspects commits since the latest reachable tag and reports the determined bump and next tag without changing files.
  • Applying the bump:
    python tools/release_version.py --write --env-file release.env --notes-file release_notes.md
    
    Updates pyproject.toml and writes environment variables (RELEASE_VERSION, RELEASE_TAG, RELEASE_BUMP) for downstream CI jobs.

Single Source of Truth

pyproject.toml is the sole source of truth for the project version. The documentation site dynamically resolves extra.version during build time via docs/main.py using get_project_metadata()["version"], preventing version drift across documentation and code.

CI Permissions & Tagging Setup

Automated tag pushes by GitLab CI require the job token to have push permissions:

  1. In GitLab under Settings -> CI/CD -> Token Access, ensure the project allows write access or configure a Project Access Token with write_repository permission stored in CI variable GITLAB_TOKEN.
  2. The release automation script runs on the default branch (main) and refuses to tag on feature branches or when the tag already exists.

Verification & Functional Validation

To ensure the project goals (Efficient Planning, Module Discovery, Data Privacy) are met, every feature must be validated against the requirements.

Automated Tests

(Automated tests were planned during Phase 4 but ultimately de-scoped for v1.0.0 due to time constraints. The project relies on rigorous manual UI/UX testing). We aim to eventually add high test coverage in tests/ to validate:

  • Search Logic: Does /module find the correct entries?
  • Filter Logic: Does /filter return valid subsets?
  • Persistence: Is the semester plan saved correctly?

Manual Verification (User Acceptance Testing)

Before merging to main, verify the following on the Test Server:

  1. Search: Query known modules (e.g., "Software Project") and check if details match the catalog.
  2. Planning: Add/Remove modules and restart the bot to ensure data persistence (Privacy/Database check).
  3. Usability: Ensure interaction flow (buttons/modals) is intuitive.

Troubleshooting & Common Issues

Pre-commit Hooks

If commitlint fails, check your commit message format.

# Good
git commit -m "fix(ui): resolve button misalignment"

# Bad
git commit -m "fixed buttons"

Linting Errors

If pylint scores are low:

  1. Read the error report (it lists file and line number).
  2. Fix the issue (e.g., add missing docstring, rename variable).
  3. Run pylint src/changed_file.py to verify the fix.

Local Documentation

To preview documentation changes locally:

mkdocs serve
# Open http://127.0.0.1:8000