GitLab & CI/CD
Dieses Dokument beschreibt, wie dieses Projekt (https://gitlab.techeve.de/techeve/form-gateway) auf dem GitLab-Server eingerichtet ist und wie man dieselbe Einrichtung für ein neues Projekt reproduziert (Referenz-Muster: techeve/uploader, techeve/lcm). Besonderheit dieses Projekts: Die Go-Jobs brauchen Zugriff auf das private ORM++-Modul — die CI setzt dafür GOPRIVATE und einen insteadOf-Rewrite mit dem CI_JOB_TOKEN (siehe .gitlab-ci.yml).
Branching-Modell
Abschnitt betitelt „Branching-Modell“main ── geschützt: kein direkter Push, nur Merge Requests aus develop, Pipeline muss grün sein, ein weiterer Entwickler muss zustimmen. Jeder Merge mit neuer VERSION erzeugt automatisch Tag + Release und rollt die .deb-Pakete auf repo.techeve.de aus.develop ── Standard-Branch: hier wird entwickelt (direkt oder über Feature-Branches mit MR nach develop).feature/* ── optionale Feature-Branches, MR-Ziel ist immer develop.Der Weg einer Änderung: feature/xyz → MR → develop → packaging/prepare-release.sh → MR → main → automatisches Release + apt-Rollout.
Einrichtung Schritt für Schritt
Abschnitt betitelt „Einrichtung Schritt für Schritt“Alle Schritte gehen über die Web-UI oder — wie hier dokumentiert — über die GitLab-REST-API mit einem Personal Access Token (Scope api):
export GITLAB=https://gitlab.techeve.de/api/v4export TOKEN=<personal-access-token>1. Projekt in der Gruppe anlegen
Abschnitt betitelt „1. Projekt in der Gruppe anlegen“# Gruppen-ID ermitteln:curl -s -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/groups?search=techeve"
# Projekt anlegen (namespace_id = Gruppen-ID):curl -s -X POST -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects" \ --data-urlencode "name=form-gateway" \ --data-urlencode "namespace_id=3" \ --data-urlencode "visibility=internal" \ --data-urlencode "initialize_with_readme=false" \ --data-urlencode "auto_devops_enabled=false"2. Code pushen, Branches anlegen
Abschnitt betitelt „2. Code pushen, Branches anlegen“git init -b main && git add -A && git commit -m "Initial import"git remote add origin https://gitlab.techeve.de/techeve/form-gateway.gitgit push -u origin maingit switch -c develop && git push -u origin developdevelop als Standard-Branch setzen (neue Clones/MRs starten dort):
curl -s -X PUT -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>" \ --data-urlencode "default_branch=develop"3. main schützen: kein direkter Push, nur Merge Requests
Abschnitt betitelt „3. main schützen: kein direkter Push, nur Merge Requests“# Eventuellen Default-Schutz entfernen, dann strikt neu anlegen:curl -s -X DELETE -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>/protected_branches/main"curl -s -X POST -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>/protected_branches" \ --data-urlencode "name=main" \ --data-urlencode "push_access_level=0" \ --data-urlencode "merge_access_level=30" \ --data-urlencode "allow_force_push=false"push_access_level=0— niemand darf direkt pushen (auch Maintainer nicht).merge_access_level=30— Developer und höher dürfen per Merge Request mergen.
develop wird ebenfalls geschützt, aber arbeitsfreundlich (Developer dürfen pushen und mergen, kein Force-Push):
curl -s -X POST -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>/protected_branches" \ --data-urlencode "name=develop" \ --data-urlencode "push_access_level=30" \ --data-urlencode "merge_access_level=30" \ --data-urlencode "allow_force_push=false"„MRs nach main nur aus develop”: GitLab kann die Quelle eines MR nicht nativ einschränken. Das erzwingt stattdessen der CI-Job check:mr-source (siehe .gitlab-ci.yml): Er schlägt in MR-Pipelines mit Ziel main fehl, wenn die Quelle nicht develop ist — und weil main nur mit grüner Pipeline gemergt werden kann, ist die Regel bindend.
4. Merge-Request-Regeln
Abschnitt betitelt „4. Merge-Request-Regeln“curl -s -X PUT -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>" \ --data-urlencode "only_allow_merge_if_pipeline_succeeds=true" \ --data-urlencode "allow_merge_on_skipped_pipeline=false" \ --data-urlencode "only_allow_merge_if_all_discussions_are_resolved=true" \ --data-urlencode "remove_source_branch_after_merge=false"- Pipeline muss grün sein — damit sind die Tests (Go, E2E, Audits) und der
check:mr-source-Job Pflicht für jeden Merge nach main. - Offene Diskussionen blockieren den Merge (Review-Disziplin).
Zustimmung eines weiteren Entwicklers: Die Zahl erforderlicher Approvals wird gesetzt mit:
curl -s -X POST -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>/approval_rules" \ --data-urlencode "name=Mindestens ein Reviewer" \ --data-urlencode "approvals_required=1"curl -s -X POST -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/projects/<id>/approvals" \ --data-urlencode "merge_requests_author_approval=false"Hinweis GitLab CE: Erzwungene Approval-Regeln (Merge-Button gesperrt bis N Zustimmungen) sind ein Premium-Feature. Auf einem CE-Server (wie hier,
enterprise: false) können Entwickler MRs zwar approven, die Zustimmung wird aber nicht hart erzwungen — die Vier-Augen-Regel gilt dann als verbindliche Team-Konvention, technisch abgesichert durchpush_access_level=0(ohne MR geht gar nichts) und die Pflicht-Pipeline. Nach einem Upgrade auf Premium greifen die obigen Befehle sofort hart.
Die CI/CD-Pipeline
Abschnitt betitelt „Die CI/CD-Pipeline“Definiert in .gitlab-ci.yml, läuft auf dem Runner mit Tag docker (Docker-Executor, Jobs laufen in Images):
| Stage | Job | Zweck |
|---|---|---|
| check | check:mr-source | MRs nach main nur aus develop (bricht sonst ab) |
| check | check:commits | erzwingt Conventional Commits in MR-Pipelines |
| check | version | nur main: liest die (auf develop vorbereitete) VERSION und prüft, ob dafür schon ein Tag existiert |
| test | frontend | npm ci + npm audit (Gate) + Vite-Build; dist/ als Artefakt |
| test | backend | go vet + go test ./... (In-Memory SQLite) |
| test | golangci-lint | Go-Linter nach .golangci.yml (errcheck, staticcheck, govet, ineffassign, unused) |
| test | govulncheck | Go-Schwachstellen-Scan (Gate) |
| test | e2e | Playwright gegen das echte Binary (auf develop/main und in MRs) |
| build | binaries | Cross-Compile: Linux amd64/arm64, Windows amd64, macOS arm64/amd64 |
| build | packages:deb | .deb-Pakete (amd64/arm64) mit systemd-Dienst via nfpm |
| release | release | nur auf main: Tag v<VERSION> + Release mit dem vorbereiteten Changelog (s.u.) |
| deploy | deploy:apt | rollt die .deb-Pakete auf repo.techeve.de aus (s.u.) |
| deploy | docs-build/docs-deploy | baut docs/ als Starlight-Seite und lädt sie nach $DOCS_URL |
Versionierung in der CI: Alle Jobs bauen mit dem Stand der VERSION-Datei aus dem Commit (auf main wird derselbe Wert vom version-Job als NEXT_VERSION durchgereicht); als Build-Nummer dient die eindeutige Pipeline-Nummer (CI_PIPELINE_IID) — die lokale .buildnumber wird in der CI nicht verändert.
Herkunft eines Releases
Abschnitt betitelt „Herkunft eines Releases“Der version-Job prüft vor jedem Release, dass der zu taggende Commit tatsächlich auf origin/main liegt (git merge-base --is-ancestor), und bricht sonst ab. Die rules binden Release und Deploy ohnehin schon an main; die Prüfung hält zusätzlich, falls die Pipeline später umgebaut wird (Tag-Pipeline, manueller Lauf, geänderte rules), und belegt die Herkunft im Job-Log.
Commit-Konvention (Conventional Commits)
Abschnitt betitelt „Commit-Konvention (Conventional Commits)“Versionsnummer und Changelog entstehen automatisch aus den Commit-Messages — deshalb erzwingt der CI-Job check:commits in jeder MR-Pipeline das Format:
typ(scope): beschreibung # scope optional| Commit-Typ | Wirkung auf die Version | Changelog-Rubrik |
|---|---|---|
feat!: / fix!: oder BREAKING CHANGE im Body | Major (2.0.0) | 💥 Breaking Changes |
feat: | Minor (1.2.0) | 🚀 Features |
fix: | Patch (1.1.1) | 🐛 Bugfixes |
perf: | Patch | ⚡ Performance |
refactor: | Patch | ♻️ Refactoring |
docs: test: ci: chore: build: style: revert: | kein Release-Auslöser | 🔧 Sonstiges |
Beispiele: feat(api): notes-endpunkt, fix(ui): navbar-umbruch auf mobilgeräten, feat!: config-format v2.
Der höchste Typ seit dem letzten Release bestimmt den Sprung (ein einziges feat! macht aus beliebig vielen fix ein Major-Release). Bestehen die Commits seit dem letzten Tag nur aus Typen ohne Release-Wirkung (docs, chore, …), entsteht beim Merge nach main kein neues Release.
Vorschau jederzeit lokal: make next-version (bzw. go run ./tools/release) zeigt die berechnete nächste Version und den Changelog-Abschnitt an. Die Logik steckt in tools/release (Go, mit Unit-Tests).
Release vorbereiten (auf develop) & veröffentlichen (auf main)
Abschnitt betitelt „Release vorbereiten (auf develop) & veröffentlichen (auf main)“Der Changelog gehört in genau den Commit, der getaggt wird. Deshalb werden Version und Changelog vor dem Merge auf develop vorbereitet — nicht nachträglich in der CI erzeugt. Damit trägt der nach main überführte Commit bereits den passenden Changelog, main hinkt nie einer Version hinterher, und die CI braucht keinen Schreib-Token.
Schritt 1 — auf develop vorbereiten (packaging/prepare-release.sh, per make prepare-release):
git switch develop && git pullmake prepare-release # Version aus den Commits seit dem letzten Tag# oder eine explizite Version erzwingen (z.B. Beta -> Finale):make prepare-release VERSION=1.0.0Das Skript ermittelt mit tools/release die nächste Version, schreibt VERSION, stellt den neuen Abschnitt oben in CHANGELOG.md fort und committet beides als release: v<version> — Version & Changelog vorbereitet. Gibt es seit dem letzten Tag keine release-relevanten Commits (nur docs/chore/…), passiert nichts — außer man gibt eine explizite Version an.
git push origin developSchritt 2 — Merge Request develop → main erstellen und nach grüner Pipeline mergen. Danach läuft auf main automatisch:
version-Job: liestNEXT_VERSIONaus der committetenVERSION, prüft obv<version>schon als Tag existiert (RELEASE_NEEDED) und schneidet den obersten Abschnitt ausCHANGELOG.mdals Release-Beschreibung heraus.binaries/packages:deb: bauen alle Plattform-Binaries und die.deb-Pakete mit dieser Version.release-Job (nur wenn der Tag noch nicht existiert): lädt Binaries und.deb-Pakete in die Generic Package Registry und erzeugt Tagv<version>+ Release mit dem Changelog als Beschreibung und den Dateien als Assets.deploy:apt: rollt die.deb-Pakete auf den Repository-Server aus (s.u.).
Kein Writeback, kein Schreib-Token: Version und Changelog stehen bereits im getaggten Commit (und über den Merge auch auf develop und main). Die CI schreibt nichts ins Repo zurück — sie liest nur. Der Release-Job kommt mit dem automatischen CI_JOB_TOKEN aus.
apt-Repository-Rollout (deploy)
Abschnitt betitelt „apt-Repository-Rollout (deploy)“Bei jedem Release rollt der Job deploy:apt die gebauten .deb-Pakete (amd64 + arm64) auf den TechEve-Repository-Server (aptly, https://repo.techeve.de) aus — danach ist Form Gateway per apt install form-gateway aus dem eigenen Repo installier- und aktualisierbar. Der Job läuft nur auf main und nur, wenn tatsächlich ein Release ansteht (RELEASE_NEEDED=true).
Benötigte CI-Variablen (bei diesem Projekt bereits als geerbte, maskierte Gruppenvariablen von „techeve” vorhanden — nichts zu tun):
| Variable | Beispiel | Zweck |
|---|---|---|
REPO_URL | https://repo.techeve.de | Basis-URL der aptly-HTTP-API |
REPO_USER | gitlab-ci | Basic-Auth-Benutzer |
REPO_PASS | (geheim) | Basic-Auth-Passwort |
Optional: REPO_NAME (Default techeve), DISTRO (Default stable), GPG_KEY (Default repo@techeve.de). Ablauf und Skript: packaging/publish-deb.sh. Die Variablen sind protected — der Job läuft deshalb nur auf einem geschützten Branch (main); fehlen sie, bricht publish-deb.sh mit einer klaren Meldung ab. SemVer-Prereleases (-beta.1) werden für das Debian-Paket zu ~beta.1 umgeschrieben, damit die Beta laut apt korrekt vor dem späteren Finale sortiert.
Paketierung (packaging/nfpm.yaml, Werkzeug nfpm): Form Gateway läuft im Paket als unprivilegierter systemd-Dienst form-gateway — Konfiguration in /etc/form-gateway/config.json (root-eigen, vom postinstall-Skript beim Erstinstall mit sicheren Zufallswerten für jwt_secret/encryption_key angelegt), Zustand (SQLite-Datenbank) in /var/lib/form-gateway. Das Admin-Passwort erzeugt und protokolliert die Anwendung selbst beim allerersten Start (journalctl -u form-gateway) — identisch zum Verhalten bei make build/Docker. postremove entfernt beim apt purge nur den System-Benutzer, niemals Datenbank oder Config (die Submission-Historie ist das Audit-Log und der Verschlüsselungs-Key steuert, ob verschlüsselte Felder je wieder lesbar werden).
Lokal bauen und die Struktur prüfen: make deb (erzeugt bin/form-gateway_<version>_{amd64,arm64}.deb).
Dependency-Updates
Abschnitt betitelt „Dependency-Updates“Dieses Projekt hat (noch) keinen Dependency-Bot (Renovate) eingerichtet — Go-Module und npm-Pakete werden manuell mit make update-deps aktualisiert. Für die Einrichtung eines automatischen Bots siehe das Referenz-Muster in techeve/lcm (docs/reference/ci-release.md, Abschnitt „Dependency-Bot (Renovate)”).
Runner-Voraussetzungen
Abschnitt betitelt „Runner-Voraussetzungen“- Ein Runner mit Docker-Executor und Tag
docker(die Jobs setzentags: [docker]). - Internetzugang für die Images (
golang:1-alpine,node:lts,alpine:3,registry.gitlab.com/gitlab-org/release-cli,debian:bookworm-slim) sowie Go-Module/npm-Pakete/Playwright-Browser. - Der Release-Job arbeitet allein mit dem automatischen
CI_JOB_TOKEN— ein Schreib-Token ist nicht nötig, weil Version und Changelog vorab auf develop committet werden. Der apt-Deploy brauchtREPO_URL/REPO_USER/REPO_PASS(s.o., bei diesem Projekt bereits vorhanden). Alle übrigen Jobs brauchen keine Secrets.
Rollen & Rechte im Projekt
Abschnitt betitelt „Rollen & Rechte im Projekt“| Rolle | darf |
|---|---|
| Developer | auf develop pushen, MRs stellen/mergen (develop und main), Reviews |
| Maintainer | zusätzlich Einstellungen, geschützte Branches verwalten |
| — (alle) | nicht direkt auf main pushen — ausnahmslos |