Zum Inhalt springen

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).

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 → developpackaging/prepare-release.sh → MR → main → automatisches Release + apt-Rollout.

Alle Schritte gehen über die Web-UI oder — wie hier dokumentiert — über die GitLab-REST-API mit einem Personal Access Token (Scope api):

Terminal-Fenster
export GITLAB=https://gitlab.techeve.de/api/v4
export TOKEN=<personal-access-token>
Terminal-Fenster
# 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"
Terminal-Fenster
git init -b main && git add -A && git commit -m "Initial import"
git remote add origin https://gitlab.techeve.de/techeve/form-gateway.git
git push -u origin main
git switch -c develop && git push -u origin develop

develop als Standard-Branch setzen (neue Clones/MRs starten dort):

Terminal-Fenster
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“
Terminal-Fenster
# 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=0niemand 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):

Terminal-Fenster
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.

Terminal-Fenster
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:

Terminal-Fenster
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 durch push_access_level=0 (ohne MR geht gar nichts) und die Pflicht-Pipeline. Nach einem Upgrade auf Premium greifen die obigen Befehle sofort hart.

Definiert in .gitlab-ci.yml, läuft auf dem Runner mit Tag docker (Docker-Executor, Jobs laufen in Images):

StageJobZweck
checkcheck:mr-sourceMRs nach main nur aus develop (bricht sonst ab)
checkcheck:commitserzwingt Conventional Commits in MR-Pipelines
checkversionnur main: liest die (auf develop vorbereitete) VERSION und prüft, ob dafür schon ein Tag existiert
testfrontendnpm ci + npm audit (Gate) + Vite-Build; dist/ als Artefakt
testbackendgo vet + go test ./... (In-Memory SQLite)
testgolangci-lintGo-Linter nach .golangci.yml (errcheck, staticcheck, govet, ineffassign, unused)
testgovulncheckGo-Schwachstellen-Scan (Gate)
teste2ePlaywright gegen das echte Binary (auf develop/main und in MRs)
buildbinariesCross-Compile: Linux amd64/arm64, Windows amd64, macOS arm64/amd64
buildpackages:deb.deb-Pakete (amd64/arm64) mit systemd-Dienst via nfpm
releasereleasenur auf main: Tag v<VERSION> + Release mit dem vorbereiteten Changelog (s.u.)
deploydeploy:aptrollt die .deb-Pakete auf repo.techeve.de aus (s.u.)
deploydocs-build/docs-deploybaut 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.

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.

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-TypWirkung auf die VersionChangelog-Rubrik
feat!: / fix!: oder BREAKING CHANGE im BodyMajor (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):

Terminal-Fenster
git switch develop && git pull
make 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.0

Das 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.

Terminal-Fenster
git push origin develop

Schritt 2 — Merge Request develop → main erstellen und nach grüner Pipeline mergen. Danach läuft auf main automatisch:

  1. version-Job: liest NEXT_VERSION aus der committeten VERSION, prüft ob v<version> schon als Tag existiert (RELEASE_NEEDED) und schneidet den obersten Abschnitt aus CHANGELOG.md als Release-Beschreibung heraus.
  2. binaries / packages:deb: bauen alle Plattform-Binaries und die .deb-Pakete mit dieser Version.
  3. release-Job (nur wenn der Tag noch nicht existiert): lädt Binaries und .deb-Pakete in die Generic Package Registry und erzeugt Tag v<version> + Release mit dem Changelog als Beschreibung und den Dateien als Assets.
  4. 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.

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):

VariableBeispielZweck
REPO_URLhttps://repo.techeve.deBasis-URL der aptly-HTTP-API
REPO_USERgitlab-ciBasic-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).

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)”).

  • Ein Runner mit Docker-Executor und Tag docker (die Jobs setzen tags: [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 braucht REPO_URL/REPO_USER/REPO_PASS (s.o., bei diesem Projekt bereits vorhanden). Alle übrigen Jobs brauchen keine Secrets.
Rolledarf
Developerauf develop pushen, MRs stellen/mergen (develop und main), Reviews
Maintainerzusätzlich Einstellungen, geschützte Branches verwalten
— (alle)nicht direkt auf main pushen — ausnahmslos